diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5193d2a..0da1b83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [windows-latest, ubuntu-latest, macos-13] + os: [windows-latest, ubuntu-latest, macos-latest] timeout-minutes: 7 steps: @@ -30,14 +30,15 @@ jobs: with: 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 - - name: Prepare FFMpeg - uses: FedericoCarboni/setup-ffmpeg@v3 + - name: Setup FFmpeg + uses: AnimMouse/setup-ffmpeg@ae28d57dabbb148eff63170b6bf7f2b60062cbae # 1.1.0 with: - ffmpeg-version: 6.0.1 - github-token: ${{ secrets.GITHUB_TOKEN }} + version: ${{ matrix.os != 'macos-latest' && '7.1' || '711' }} + token: ${{ github.token }} - name: Test with dotnet run: dotnet test FFMpegCore.sln --collect "XPlat Code Coverage" --logger GitHubActions diff --git a/Directory.Build.props b/Directory.Build.props index 628195a..0ce88ff 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,26 +1,26 @@ - - netstandard2.0 - en - 5.0.0.0 - default - enable - true - enable + + netstandard2.0 + en + 5.0.0.0 + default + enable + true + enable - GitHub - https://github.com/rosenbjerg/FFMpegCore - https://github.com/rosenbjerg/FFMpegCore - MIT - en + GitHub + https://github.com/rosenbjerg/FFMpegCore + https://github.com/rosenbjerg/FFMpegCore + MIT + en - true - true - snupkg - true - + true + true + snupkg + true + - - true - + + true + \ No newline at end of file diff --git a/FFMpegCore.Examples/FFMpegCore.Examples.csproj b/FFMpegCore.Examples/FFMpegCore.Examples.csproj index 1aa7d46..4a1fa12 100644 --- a/FFMpegCore.Examples/FFMpegCore.Examples.csproj +++ b/FFMpegCore.Examples/FFMpegCore.Examples.csproj @@ -7,7 +7,7 @@ - + diff --git a/FFMpegCore.Examples/Program.cs b/FFMpegCore.Examples/Program.cs index d7abde4..15db87c 100644 --- a/FFMpegCore.Examples/Program.cs +++ b/FFMpegCore.Examples/Program.cs @@ -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); } -IVideoFrame GetNextFrame() => throw new NotImplementedException(); +IVideoFrame GetNextFrame() +{ + throw new NotImplementedException(); +} + { IEnumerable CreateFrames(int count) { @@ -100,10 +104,11 @@ IVideoFrame GetNextFrame() => throw new NotImplementedException(); } } - var videoFramesSource = new RawVideoPipeSource(CreateFrames(64)) //pass IEnumerable or IEnumerator to constructor of RawVideoPipeSource - { - FrameRate = 30 //set source frame rate - }; + var videoFramesSource = + new RawVideoPipeSource(CreateFrames(64)) //pass IEnumerable or IEnumerator to constructor of RawVideoPipeSource + { + FrameRate = 30 //set source frame rate + }; await FFMpegArguments .FromPipeInput(videoFramesSource) .OutputToFile(outputPath, false, options => options diff --git a/FFMpegCore.Extensions.Downloader/Enums/FFMpegBinaries.cs b/FFMpegCore.Extensions.Downloader/Enums/FFMpegBinaries.cs new file mode 100644 index 0000000..c3be69f --- /dev/null +++ b/FFMpegCore.Extensions.Downloader/Enums/FFMpegBinaries.cs @@ -0,0 +1,9 @@ +namespace FFMpegCore.Extensions.Downloader.Enums; + +[Flags] +public enum FFMpegBinaries : ushort +{ + FFMpeg = 1, + FFProbe = 2, + FFPlay = 4 +} diff --git a/FFMpegCore.Extensions.Downloader/Enums/FFMpegVersions.cs b/FFMpegCore.Extensions.Downloader/Enums/FFMpegVersions.cs new file mode 100644 index 0000000..c9f5dd3 --- /dev/null +++ b/FFMpegCore.Extensions.Downloader/Enums/FFMpegVersions.cs @@ -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 +} diff --git a/FFMpegCore.Extensions.Downloader/Enums/SupportedPlatforms.cs b/FFMpegCore.Extensions.Downloader/Enums/SupportedPlatforms.cs new file mode 100644 index 0000000..0378f3e --- /dev/null +++ b/FFMpegCore.Extensions.Downloader/Enums/SupportedPlatforms.cs @@ -0,0 +1,13 @@ +namespace FFMpegCore.Extensions.Downloader.Enums; + +public enum SupportedPlatforms : ushort +{ + Windows64, + Windows32, + Linux64, + Linux32, + LinuxArmhf, + LinuxArmel, + LinuxArm64, + Osx64 +} diff --git a/FFMpegCore.Extensions.Downloader/Exceptions/FFMpegDownloaderException.cs b/FFMpegCore.Extensions.Downloader/Exceptions/FFMpegDownloaderException.cs new file mode 100644 index 0000000..8355c4c --- /dev/null +++ b/FFMpegCore.Extensions.Downloader/Exceptions/FFMpegDownloaderException.cs @@ -0,0 +1,18 @@ +namespace FFMpegCore.Extensions.Downloader.Exceptions; + +/// +/// Custom exception for FFMpegDownloader +/// +public class FFMpegDownloaderException : Exception +{ + public readonly string Detail = ""; + + public FFMpegDownloaderException(string message) : base(message) + { + } + + public FFMpegDownloaderException(string message, string detail) : base(message) + { + Detail = detail; + } +} diff --git a/FFMpegCore.Extensions.Downloader/Extensions/EnumExtensions.cs b/FFMpegCore.Extensions.Downloader/Extensions/EnumExtensions.cs new file mode 100644 index 0000000..3336f11 --- /dev/null +++ b/FFMpegCore.Extensions.Downloader/Extensions/EnumExtensions.cs @@ -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(this TEnum input) where TEnum : Enum + { + return Enum.GetValues(input.GetType()) + .Cast() + .Where(input.HasFlag) + .Cast() + .ToArray(); + } +} diff --git a/FFMpegCore.Extensions.Downloader/FFMpegCore.Extensions.Downloader.csproj b/FFMpegCore.Extensions.Downloader/FFMpegCore.Extensions.Downloader.csproj new file mode 100644 index 0000000..2715c89 --- /dev/null +++ b/FFMpegCore.Extensions.Downloader/FFMpegCore.Extensions.Downloader.csproj @@ -0,0 +1,19 @@ + + + + true + FFMpeg downloader extension for FFMpegCore + 5.0.0 + ../nupkg + + - Updated dependencies + + ffmpeg ffprobe convert video audio mediafile resize analyze download install + Kerry Cao, Malte Rosenbjerg + + + + + + + diff --git a/FFMpegCore.Extensions.Downloader/FFMpegDownloader.cs b/FFMpegCore.Extensions.Downloader/FFMpegDownloader.cs new file mode 100644 index 0000000..f616c31 --- /dev/null +++ b/FFMpegCore.Extensions.Downloader/FFMpegDownloader.cs @@ -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 +{ + /// + /// Download the latest FFMpeg suite binaries for current platform + /// + /// used to explicitly state the version of binary you want to download + /// used to explicitly state the binaries you want to download (ffmpeg, ffprobe, ffplay) + /// used for specifying binary folder to download binaries into. If not provided, GlobalFFOptions are used + /// used to explicitly state the os and architecture you want to download + /// a list of the binaries that have been successfully downloaded + public static async Task> 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(); + 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 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(jsonString); + + return versionInfo ?? + throw new FFMpegDownloaderException($"Failed to deserialize version info from {versionUri}", jsonString); + } + + private static IEnumerable 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; + } + } + } +} diff --git a/FFMpegCore.Extensions.Downloader/Models/BinaryInfo.cs b/FFMpegCore.Extensions.Downloader/Models/BinaryInfo.cs new file mode 100644 index 0000000..d02cb14 --- /dev/null +++ b/FFMpegCore.Extensions.Downloader/Models/BinaryInfo.cs @@ -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? Windows64 { get; set; } + + [JsonPropertyName("windows-32")] public Dictionary? Windows32 { get; set; } + + [JsonPropertyName("linux-32")] public Dictionary? Linux32 { get; set; } + + [JsonPropertyName("linux-64")] public Dictionary? Linux64 { get; set; } + + [JsonPropertyName("linux-armhf")] public Dictionary? LinuxArmhf { get; set; } + + [JsonPropertyName("linux-armel")] public Dictionary? LinuxArmel { get; set; } + + [JsonPropertyName("linux-arm64")] public Dictionary? LinuxArm64 { get; set; } + + [JsonPropertyName("osx-64")] public Dictionary? Osx64 { get; set; } + + /// + /// Automatically get the compatible download info for current os and architecture + /// + /// + /// + /// + /// + public Dictionary? 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"); + } +} diff --git a/FFMpegCore.Extensions.Downloader/Models/VersionInfo.cs b/FFMpegCore.Extensions.Downloader/Models/VersionInfo.cs new file mode 100644 index 0000000..ef24f62 --- /dev/null +++ b/FFMpegCore.Extensions.Downloader/Models/VersionInfo.cs @@ -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; } +} diff --git a/FFMpegCore.Extensions.SkiaSharp/BitmapExtensions.cs b/FFMpegCore.Extensions.SkiaSharp/BitmapExtensions.cs index 34e303a..c205595 100644 --- a/FFMpegCore.Extensions.SkiaSharp/BitmapExtensions.cs +++ b/FFMpegCore.Extensions.SkiaSharp/BitmapExtensions.cs @@ -1,27 +1,26 @@ using 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 - } +namespace FFMpegCore.Extensions.SkiaSharp; - 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); - } - finally - { - if (File.Exists(destination)) - { - File.Delete(destination); - } + File.Delete(destination); } } } diff --git a/FFMpegCore.Extensions.SkiaSharp/BitmapVideoFrameWrapper.cs b/FFMpegCore.Extensions.SkiaSharp/BitmapVideoFrameWrapper.cs index 7bb98fb..8f6aaa8 100644 --- a/FFMpegCore.Extensions.SkiaSharp/BitmapVideoFrameWrapper.cs +++ b/FFMpegCore.Extensions.SkiaSharp/BitmapVideoFrameWrapper.cs @@ -1,59 +1,58 @@ using FFMpegCore.Pipes; 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)); - Format = ConvertStreamFormat(bitmap.ColorType); - } - - 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); - } - - 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}"); - } + 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}"); } } } diff --git a/FFMpegCore.Extensions.SkiaSharp/FFMpegCore.Extensions.SkiaSharp.csproj b/FFMpegCore.Extensions.SkiaSharp/FFMpegCore.Extensions.SkiaSharp.csproj index 3609ddf..5dccbfe 100644 --- a/FFMpegCore.Extensions.SkiaSharp/FFMpegCore.Extensions.SkiaSharp.csproj +++ b/FFMpegCore.Extensions.SkiaSharp/FFMpegCore.Extensions.SkiaSharp.csproj @@ -1,23 +1,25 @@ - - true - Image extension for FFMpegCore using SkiaSharp - 5.0.2 - ../nupkg - Bump dependencies - ffmpeg ffprobe convert video audio mediafile resize analyze muxing skiasharp - Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev, Dimitri Vranken - true - + + true + Image extension for FFMpegCore using SkiaSharp + 5.0.3 + ../nupkg + + - Updated dependencies + + ffmpeg ffprobe convert video audio image mediafile resize analyze muxing skia skiasharp + Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev, Dimitri Vranken + true + - - - - + + + + - - - + + + diff --git a/FFMpegCore.Extensions.SkiaSharp/FFMpegImage.cs b/FFMpegCore.Extensions.SkiaSharp/FFMpegImage.cs index 69929d3..f412844 100644 --- a/FFMpegCore.Extensions.SkiaSharp/FFMpegImage.cs +++ b/FFMpegCore.Extensions.SkiaSharp/FFMpegImage.cs @@ -2,56 +2,57 @@ using FFMpegCore.Pipes; using SkiaSharp; -namespace FFMpegCore.Extensions.SkiaSharp +namespace FFMpegCore.Extensions.SkiaSharp; + +public static class FFMpegImage { - public static class FFMpegImage + /// + /// Saves a 'png' thumbnail to an in-memory bitmap + /// + /// Source video file. + /// Seek position where the thumbnail should be taken. + /// Thumbnail size. If width or height equal 0, the other will be computed automatically. + /// Selected video stream index. + /// Input file index + /// Bitmap with the requested snapshot. + public static SKBitmap Snapshot(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0) { - /// - /// Saves a 'png' thumbnail to an in-memory bitmap - /// - /// Source video file. - /// Seek position where the thumbnail should be taken. - /// Thumbnail size. If width or height equal 0, the other will be computed automatically. - /// Selected video stream index. - /// Input file index - /// Bitmap with the requested snapshot. - 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(); + var source = FFProbe.Analyse(input); + var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); + using var ms = new MemoryStream(); - arguments - .OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options - .ForceFormat("rawvideo"))) - .ProcessSynchronously(); + arguments + .OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options + .ForceFormat("rawvideo"))) + .ProcessSynchronously(); - ms.Position = 0; - using var bitmap = SKBitmap.Decode(ms); - return bitmap.Copy(); - } - /// - /// Saves a 'png' thumbnail to an in-memory bitmap - /// - /// Source video file. - /// Seek position where the thumbnail should be taken. - /// Thumbnail size. If width or height equal 0, the other will be computed automatically. - /// Selected video stream index. - /// Input file index - /// Bitmap with the requested snapshot. - public static async Task 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; + using var bitmap = SKBitmap.Decode(ms); + return bitmap.Copy(); + } - await arguments - .OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options - .ForceFormat("rawvideo"))) - .ProcessAsynchronously(); + /// + /// Saves a 'png' thumbnail to an in-memory bitmap + /// + /// Source video file. + /// Seek position where the thumbnail should be taken. + /// Thumbnail size. If width or height equal 0, the other will be computed automatically. + /// Selected video stream index. + /// Input file index + /// Bitmap with the requested snapshot. + public static async Task 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; - return SKBitmap.Decode(ms); - } + await arguments + .OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options + .ForceFormat("rawvideo"))) + .ProcessAsynchronously(); + + ms.Position = 0; + return SKBitmap.Decode(ms); } } diff --git a/FFMpegCore.Extensions.System.Drawing.Common/BitmapExtensions.cs b/FFMpegCore.Extensions.System.Drawing.Common/BitmapExtensions.cs index 14cecaa..75131bd 100644 --- a/FFMpegCore.Extensions.System.Drawing.Common/BitmapExtensions.cs +++ b/FFMpegCore.Extensions.System.Drawing.Common/BitmapExtensions.cs @@ -1,23 +1,22 @@ 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"; - poster.Save(destination); - try + return FFMpeg.PosterWithAudio(destination, audio, output); + } + finally + { + if (File.Exists(destination)) { - return FFMpeg.PosterWithAudio(destination, audio, output); - } - finally - { - if (File.Exists(destination)) - { - File.Delete(destination); - } + File.Delete(destination); } } } diff --git a/FFMpegCore.Extensions.System.Drawing.Common/BitmapVideoFrameWrapper.cs b/FFMpegCore.Extensions.System.Drawing.Common/BitmapVideoFrameWrapper.cs index 5462ca2..55a3188 100644 --- a/FFMpegCore.Extensions.System.Drawing.Common/BitmapVideoFrameWrapper.cs +++ b/FFMpegCore.Extensions.System.Drawing.Common/BitmapVideoFrameWrapper.cs @@ -3,85 +3,84 @@ using System.Drawing.Imaging; using System.Runtime.InteropServices; 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)); - Format = ConvertStreamFormat(bitmap.PixelFormat); + var buffer = new byte[data.Stride * data.Height]; + Marshal.Copy(data.Scan0, buffer, 0, buffer.Length); + stream.Write(buffer, 0, buffer.Length); } - - public void Serialize(Stream stream) + finally { - var data = Source.LockBits(new Rectangle(0, 0, Width, Height), ImageLockMode.ReadOnly, Source.PixelFormat); - - 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); - } + 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); - - try - { - 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); - } + 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); } - - public void Dispose() + finally { - 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.Format16bppRgb555: - return "bgr555le"; - case PixelFormat.Format16bppRgb565: - return "bgr565le"; - case PixelFormat.Format24bppRgb: - return "bgr24"; - case PixelFormat.Format32bppArgb: - return "bgra"; - case PixelFormat.Format32bppPArgb: - //This is not really same as argb32 - return "argb"; - case PixelFormat.Format32bppRgb: - return "rgba"; - case PixelFormat.Format48bppRgb: - return "rgb48le"; - default: - throw new NotSupportedException($"Not supported pixel format {fmt}"); - } + case PixelFormat.Format16bppGrayScale: + return "gray16le"; + case PixelFormat.Format16bppRgb555: + return "bgr555le"; + case PixelFormat.Format16bppRgb565: + return "bgr565le"; + case PixelFormat.Format24bppRgb: + return "bgr24"; + case PixelFormat.Format32bppArgb: + return "bgra"; + case PixelFormat.Format32bppPArgb: + //This is not really same as argb32 + return "argb"; + case PixelFormat.Format32bppRgb: + return "rgba"; + case PixelFormat.Format48bppRgb: + return "rgb48le"; + default: + throw new NotSupportedException($"Not supported pixel format {fmt}"); } } } diff --git a/FFMpegCore.Extensions.System.Drawing.Common/FFMpegCore.Extensions.System.Drawing.Common.csproj b/FFMpegCore.Extensions.System.Drawing.Common/FFMpegCore.Extensions.System.Drawing.Common.csproj index 3a45aa5..fb09ab7 100644 --- a/FFMpegCore.Extensions.System.Drawing.Common/FFMpegCore.Extensions.System.Drawing.Common.csproj +++ b/FFMpegCore.Extensions.System.Drawing.Common/FFMpegCore.Extensions.System.Drawing.Common.csproj @@ -3,16 +3,18 @@ true Image extension for FFMpegCore using System.Common.Drawing - 5.0.2 + 5.0.3 ../nupkg - Bump dependencies - ffmpeg ffprobe convert video audio mediafile resize analyze muxing + + - Updated dependencies + + ffmpeg ffprobe convert video audio image mediafile resize analyze muxing Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev true - + diff --git a/FFMpegCore.Extensions.System.Drawing.Common/FFMpegImage.cs b/FFMpegCore.Extensions.System.Drawing.Common/FFMpegImage.cs index c946507..1c7f965 100644 --- a/FFMpegCore.Extensions.System.Drawing.Common/FFMpegImage.cs +++ b/FFMpegCore.Extensions.System.Drawing.Common/FFMpegImage.cs @@ -1,57 +1,57 @@ using System.Drawing; using FFMpegCore.Pipes; -namespace FFMpegCore.Extensions.System.Drawing.Common +namespace FFMpegCore.Extensions.System.Drawing.Common; + +public static class FFMpegImage { - public static class FFMpegImage + /// + /// Saves a 'png' thumbnail to an in-memory bitmap + /// + /// Source video file. + /// Seek position where the thumbnail should be taken. + /// Thumbnail size. If width or height equal 0, the other will be computed automatically. + /// Selected video stream index. + /// Input file index + /// Bitmap with the requested snapshot. + public static Bitmap Snapshot(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0) { - /// - /// Saves a 'png' thumbnail to an in-memory bitmap - /// - /// Source video file. - /// Seek position where the thumbnail should be taken. - /// Thumbnail size. If width or height equal 0, the other will be computed automatically. - /// Selected video stream index. - /// Input file index - /// Bitmap with the requested snapshot. - 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(); + var source = FFProbe.Analyse(input); + var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); + using var ms = new MemoryStream(); - arguments - .OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options - .ForceFormat("rawvideo"))) - .ProcessSynchronously(); + arguments + .OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options + .ForceFormat("rawvideo"))) + .ProcessSynchronously(); - ms.Position = 0; - using var bitmap = new Bitmap(ms); - return bitmap.Clone(new Rectangle(0, 0, bitmap.Width, bitmap.Height), bitmap.PixelFormat); - } + ms.Position = 0; + using var bitmap = new Bitmap(ms); + return bitmap.Clone(new Rectangle(0, 0, bitmap.Width, bitmap.Height), bitmap.PixelFormat); + } - /// - /// Saves a 'png' thumbnail to an in-memory bitmap - /// - /// Source video file. - /// Seek position where the thumbnail should be taken. - /// Thumbnail size. If width or height equal 0, the other will be computed automatically. - /// Selected video stream index. - /// Input file index - /// Bitmap with the requested snapshot. - public static async Task 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(); + /// + /// Saves a 'png' thumbnail to an in-memory bitmap + /// + /// Source video file. + /// Seek position where the thumbnail should be taken. + /// Thumbnail size. If width or height equal 0, the other will be computed automatically. + /// Selected video stream index. + /// Input file index + /// Bitmap with the requested snapshot. + public static async Task 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 - .OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options - .ForceFormat("rawvideo"))) - .ProcessAsynchronously(); + await arguments + .OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options + .ForceFormat("rawvideo"))) + .ProcessAsynchronously(); - ms.Position = 0; - return new Bitmap(ms); - } + ms.Position = 0; + return new Bitmap(ms); } } diff --git a/FFMpegCore.Test/ArgumentBuilderTest.cs b/FFMpegCore.Test/ArgumentBuilderTest.cs index ce39c9d..53999c3 100644 --- a/FFMpegCore.Test/ArgumentBuilderTest.cs +++ b/FFMpegCore.Test/ArgumentBuilderTest.cs @@ -1,697 +1,722 @@ using System.Drawing; using FFMpegCore.Arguments; using FFMpegCore.Enums; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using FFMpegCore.Pipes; -namespace FFMpegCore.Test +namespace FFMpegCore.Test; + +[TestClass] +public class ArgumentBuilderTest { - [TestClass] - public class ArgumentBuilderTest + private readonly string[] _concatFiles = { "1.mp4", "2.mp4", "3.mp4", "4.mp4" }; + private readonly int _macOsMaxPipePathLength = 104; + private readonly string[] _multiFiles = { "1.mp3", "2.mp3", "3.mp3", "4.mp3" }; + + [TestMethod] + public void Builder_BuildString_IO_1() { - private readonly string[] _concatFiles = { "1.mp4", "2.mp4", "3.mp4", "4.mp4" }; - private readonly string[] _multiFiles = { "1.mp3", "2.mp3", "3.mp3", "4.mp3" }; + var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4").Arguments; + Assert.AreEqual("-i \"input.mp4\" \"output.mp4\" -y", str); + } - [TestMethod] - public void Builder_BuildString_IO_1() + [TestMethod] + public void Builder_BuildString_Scale() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", true, opt => opt + .WithVideoFilters(filterOptions => filterOptions + .Scale(VideoSize.Hd))) + .Arguments; + Assert.AreEqual("-i \"input.mp4\" -vf \"scale=-1:720\" \"output.mp4\" -y", str); + } + + [TestMethod] + public void Builder_BuildString_AudioCodec() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", true, opt => opt.WithAudioCodec(AudioCodec.Aac)).Arguments; + Assert.AreEqual("-i \"input.mp4\" -c:a aac \"output.mp4\" -y", str); + } + + [TestMethod] + public void Builder_BuildString_AudioBitrate() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", true, opt => opt.WithAudioBitrate(AudioQuality.Normal)).Arguments; + Assert.AreEqual("-i \"input.mp4\" -b:a 128k \"output.mp4\" -y", str); + } + + [TestMethod] + public void Builder_BuildString_Quiet() + { + var str = FFMpegArguments.FromFileInput("input.mp4").WithGlobalOptions(opt => opt.WithVerbosityLevel()) + .OutputToFile("output.mp4", false).Arguments; + Assert.AreEqual("-hide_banner -loglevel error -i \"input.mp4\" \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_AudioCodec_Fluent() + { + var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, + opt => opt.WithAudioCodec(AudioCodec.Aac).WithAudioBitrate(128)).Arguments; + Assert.AreEqual("-i \"input.mp4\" -c:a aac -b:a 128k \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_BitStream() + { + var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, + opt => opt.WithBitStreamFilter(Channel.Audio, Filter.H264_Mp4ToAnnexB)).Arguments; + Assert.AreEqual("-i \"input.mp4\" -bsf:a h264_mp4toannexb \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_HardwareAcceleration_Auto() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.WithHardwareAcceleration()).Arguments; + Assert.AreEqual("-i \"input.mp4\" -hwaccel auto \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_HardwareAcceleration_Specific() + { + var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, + opt => opt.WithHardwareAcceleration(HardwareAccelerationDevice.CUVID)).Arguments; + Assert.AreEqual("-i \"input.mp4\" -hwaccel cuvid \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_Concat() + { + var str = FFMpegArguments.FromConcatInput(_concatFiles).OutputToFile("output.mp4", false).Arguments; + Assert.AreEqual("-i \"concat:1.mp4|2.mp4|3.mp4|4.mp4\" \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_Copy_Audio() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.CopyChannel(Channel.Audio)).Arguments; + Assert.AreEqual("-i \"input.mp4\" -c:a copy \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_Copy_Video() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.CopyChannel(Channel.Video)).Arguments; + Assert.AreEqual("-i \"input.mp4\" -c:v copy \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_Copy_Both() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.CopyChannel()).Arguments; + Assert.AreEqual("-i \"input.mp4\" -c:a copy -c:v copy \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_DisableChannel_Audio() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.DisableChannel(Channel.Audio)).Arguments; + Assert.AreEqual("-i \"input.mp4\" -an \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_DisableChannel_Video() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.DisableChannel(Channel.Video)).Arguments; + Assert.AreEqual("-i \"input.mp4\" -vn \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_AudioSamplingRate_Default() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.WithAudioSamplingRate()).Arguments; + Assert.AreEqual("-i \"input.mp4\" -ar 48000 \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_AudioSamplingRate() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.WithAudioSamplingRate(44000)).Arguments; + Assert.AreEqual("-i \"input.mp4\" -ar 44000 \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_VariableBitrate() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.WithVariableBitrate(5)).Arguments; + Assert.AreEqual("-i \"input.mp4\" -vbr 5 \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_Faststart() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.WithFastStart()).Arguments; + Assert.AreEqual("-i \"input.mp4\" -movflags faststart \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_Overwrite() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.OverwriteExisting()).Arguments; + Assert.AreEqual("-i \"input.mp4\" -y \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_RemoveMetadata() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.WithoutMetadata()).Arguments; + Assert.AreEqual("-i \"input.mp4\" -map_metadata -1 \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_Transpose() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt + .WithVideoFilters(filterOptions => filterOptions + .Transpose(Transposition.CounterClockwise90))) + .Arguments; + Assert.AreEqual("-i \"input.mp4\" -vf \"transpose=2\" \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_Mirroring() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt + .WithVideoFilters(filterOptions => filterOptions + .Mirror(Mirroring.Horizontal))) + .Arguments; + Assert.AreEqual("-i \"input.mp4\" -vf \"hflip\" \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_TransposeScale() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt + .WithVideoFilters(filterOptions => filterOptions + .Transpose(Transposition.CounterClockwise90) + .Scale(200, 300))) + .Arguments; + Assert.AreEqual("-i \"input.mp4\" -vf \"transpose=2, scale=200:300\" \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_ForceFormat() + { + var str = FFMpegArguments.FromFileInput("input.mp4", false, opt => opt.ForceFormat(VideoType.Mp4)) + .OutputToFile("output.mp4", false, opt => opt.ForceFormat(VideoType.Mp4)).Arguments; + Assert.AreEqual("-f mp4 -i \"input.mp4\" -f mp4 \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_FrameOutputCount() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.WithFrameOutputCount(50)).Arguments; + Assert.AreEqual("-i \"input.mp4\" -vframes 50 \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_VideoStreamNumber() + { + var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.SelectStream(1)).Arguments; + Assert.AreEqual("-i \"input.mp4\" -map 0:1 \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_FrameRate() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.WithFramerate(50)).Arguments; + Assert.AreEqual("-i \"input.mp4\" -r 50 \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_Loop() + { + var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.Loop(50)) + .Arguments; + Assert.AreEqual("-i \"input.mp4\" -loop 50 \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_Seek() + { + var str = FFMpegArguments.FromFileInput("input.mp4", false, opt => opt.Seek(TimeSpan.FromSeconds(10))) + .OutputToFile("output.mp4", false, opt => opt.Seek(TimeSpan.FromSeconds(10))).Arguments; + Assert.AreEqual("-ss 00:00:10.000 -i \"input.mp4\" -ss 00:00:10.000 \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_EndSeek() + { + var str = FFMpegArguments.FromFileInput("input.mp4", false, opt => opt.EndSeek(TimeSpan.FromSeconds(10))) + .OutputToFile("output.mp4", false, opt => opt.EndSeek(TimeSpan.FromSeconds(10))).Arguments; + Assert.AreEqual("-to 00:00:10.000 -i \"input.mp4\" -to 00:00:10.000 \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_Shortest() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.UsingShortest()).Arguments; + Assert.AreEqual("-i \"input.mp4\" -shortest \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_Size() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.Resize(1920, 1080)).Arguments; + Assert.AreEqual("-i \"input.mp4\" -s 1920x1080 \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_Speed() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.WithSpeedPreset(Speed.Fast)).Arguments; + Assert.AreEqual("-i \"input.mp4\" -preset fast \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_DrawtextFilter() + { + var str = FFMpegArguments + .FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt + .WithVideoFilters(filterOptions => filterOptions + .DrawText(DrawTextOptions + .Create("Stack Overflow", "/path/to/font.ttf") + .WithParameter("fontcolor", "white") + .WithParameter("fontsize", "24") + .WithParameter("box", "1") + .WithParameter("boxcolor", "black@0.5") + .WithParameter("boxborderw", "5") + .WithParameter("x", "(w-text_w)/2") + .WithParameter("y", "(h-text_h)/2")))) + .Arguments; + + Assert.AreEqual( + "-i \"input.mp4\" -vf \"drawtext=text='Stack Overflow':fontfile=/path/to/font.ttf:fontcolor=white:fontsize=24:box=1:boxcolor=black@0.5:boxborderw=5:x=(w-text_w)/2:y=(h-text_h)/2\" \"output.mp4\"", + str); + } + + [TestMethod] + public void Builder_BuildString_DrawtextFilter_Alt() + { + var str = FFMpegArguments + .FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt + .WithVideoFilters(filterOptions => filterOptions + .DrawText(DrawTextOptions + .Create("Stack Overflow", "/path/to/font.ttf", ("fontcolor", "white"), ("fontsize", "24"))))) + .Arguments; + + Assert.AreEqual( + "-i \"input.mp4\" -vf \"drawtext=text='Stack Overflow':fontfile=/path/to/font.ttf:fontcolor=white:fontsize=24\" \"output.mp4\"", + str); + } + + [TestMethod] + public void Builder_BuildString_SubtitleHardBurnFilter() + { + var str = FFMpegArguments + .FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt + .WithVideoFilters(filterOptions => filterOptions + .HardBurnSubtitle(SubtitleHardBurnOptions + .Create("sample.srt") + .SetCharacterEncoding("UTF-8") + .SetOriginalSize(1366, 768) + .SetSubtitleIndex(0) + .WithStyle(StyleOptions.Create() + .WithParameter("FontName", "DejaVu Serif") + .WithParameter("PrimaryColour", "&HAA00FF00"))))) + .Arguments; + + Assert.AreEqual( + "-i \"input.mp4\" -vf \"subtitles='sample.srt':charenc=UTF-8:original_size=1366x768:stream_index=0:force_style='FontName=DejaVu Serif\\,PrimaryColour=&HAA00FF00'\" \"output.mp4\"", + str); + } + + [TestMethod] + public void Builder_BuildString_SubtitleHardBurnFilterFixedPaths() + { + var str = FFMpegArguments + .FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt + .WithVideoFilters(filterOptions => filterOptions + .HardBurnSubtitle(SubtitleHardBurnOptions + .Create(@"sample( \ : [ ] , ' ).srt")))) + .Arguments; + + Assert.AreEqual(@"-i ""input.mp4"" -vf ""subtitles='sample( \\ \: \[ \] \, '\\\'' ).srt'"" ""output.mp4""", + str); + } + + [TestMethod] + public void Builder_BuildString_StartNumber() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.WithStartNumber(50)).Arguments; + Assert.AreEqual("-i \"input.mp4\" -start_number 50 \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_Threads_1() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.UsingThreads(50)).Arguments; + Assert.AreEqual("-i \"input.mp4\" -threads 50 \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_Threads_2() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.UsingMultithreading(true)).Arguments; + Assert.AreEqual($"-i \"input.mp4\" -threads {Environment.ProcessorCount} \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_Codec() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.WithVideoCodec(VideoCodec.LibX264)).Arguments; + Assert.AreEqual("-i \"input.mp4\" -c:v libx264 \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_Codec_Override() + { + var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", true, + opt => opt.WithVideoCodec(VideoCodec.LibX264).ForcePixelFormat("yuv420p")).Arguments; + Assert.AreEqual("-i \"input.mp4\" -c:v libx264 -pix_fmt yuv420p \"output.mp4\" -y", str); + } + + [TestMethod] + public void Builder_BuildString_Duration() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.WithDuration(TimeSpan.FromSeconds(20))).Arguments; + Assert.AreEqual("-i \"input.mp4\" -t 00:00:20 \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_Raw() + { + var str = FFMpegArguments.FromFileInput("input.mp4", false, opt => opt.WithCustomArgument(null!)) + .OutputToFile("output.mp4", false, opt => opt.WithCustomArgument(null!)).Arguments; + Assert.AreEqual(" -i \"input.mp4\" \"output.mp4\"", str); + + str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.WithCustomArgument("-acodec copy")).Arguments; + Assert.AreEqual("-i \"input.mp4\" -acodec copy \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_ForcePixelFormat() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt.ForcePixelFormat("yuv444p")).Arguments; + Assert.AreEqual("-i \"input.mp4\" -pix_fmt yuv444p \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_PanAudioFilterChannelNumber() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, + opt => opt.WithAudioFilters(filterOptions => filterOptions.Pan(2, "c0=c1", "c1=c1"))) + .Arguments; + + Assert.AreEqual("-i \"input.mp4\" -af \"pan=2c|c0=c1|c1=c1\" \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_PanAudioFilterChannelLayout() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, + opt => opt.WithAudioFilters(filterOptions => filterOptions.Pan("stereo", "c0=c0", "c1=c1"))) + .Arguments; + + Assert.AreEqual("-i \"input.mp4\" -af \"pan=stereo|c0=c0|c1=c1\" \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_PanAudioFilterChannelNoOutputDefinition() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, + opt => opt.WithAudioFilters(filterOptions => filterOptions.Pan("stereo"))) + .Arguments; + + Assert.AreEqual("-i \"input.mp4\" -af \"pan=stereo\" \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_DynamicAudioNormalizerDefaultFormat() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, + opt => opt.WithAudioFilters(filterOptions => filterOptions.DynamicNormalizer())) + .Arguments; + + Assert.AreEqual("-i \"input.mp4\" -af \"dynaudnorm=f=500:g=31:p=0.95:m=10.0:r=0.0:n=1:c=0:b=0:s=0.0\" \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_DynamicAudioNormalizerWithValuesFormat() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, + opt => opt.WithAudioFilters(filterOptions => filterOptions.DynamicNormalizer(125, 13, 0.9215, 5.124, 0.5458, false, true, true, 0.3333333))) + .Arguments; + + Assert.AreEqual("-i \"input.mp4\" -af \"dynaudnorm=f=125:g=13:p=0.92:m=5.1:r=0.5:n=0:c=1:b=1:s=0.3\" \"output.mp4\"", str); + } + + [TestMethod] + public void Builder_BuildString_Audible_AAXC_Decryption() + { + var str = FFMpegArguments.FromFileInput("input.aaxc", false, x => x.WithAudibleEncryptionKeys("123", "456")) + .MapMetaData() + .OutputToFile("output.m4b", true, x => x.WithTagVersion().DisableChannel(Channel.Video).CopyChannel(Channel.Audio)) + .Arguments; + + Assert.AreEqual("-audible_key 123 -audible_iv 456 -i \"input.aaxc\" -map_metadata 0 -id3v2_version 3 -vn -c:a copy \"output.m4b\" -y", str); + } + + [TestMethod] + public void Builder_BuildString_PadFilter() + { + var str = FFMpegArguments + .FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt + .WithVideoFilters(filterOptions => filterOptions + .Pad(PadOptions + .Create("max(iw,ih)", "ow") + .WithParameter("x", "(ow-iw)/2") + .WithParameter("y", "(oh-ih)/2") + .WithParameter("color", "violet") + .WithParameter("eval", "frame")))) + .Arguments; + + Assert.AreEqual( + "-i \"input.mp4\" -vf \"pad=width=max(iw\\,ih):height=ow:x=(ow-iw)/2:y=(oh-ih)/2:color=violet:eval=frame\" \"output.mp4\"", + str); + } + + [TestMethod] + public void Builder_BuildString_PadFilter_Alt() + { + var str = FFMpegArguments + .FromFileInput("input.mp4") + .OutputToFile("output.mp4", false, opt => opt + .WithVideoFilters(filterOptions => filterOptions + .Pad(PadOptions + .Create("4/3") + .WithParameter("x", "(ow-iw)/2") + .WithParameter("y", "(oh-ih)/2") + .WithParameter("color", "violet") + .WithParameter("eval", "frame")))) + .Arguments; + + Assert.AreEqual( + "-i \"input.mp4\" -vf \"pad=aspect=4/3:x=(ow-iw)/2:y=(oh-ih)/2:color=violet:eval=frame\" \"output.mp4\"", + str); + } + + [TestMethod] + public void Builder_BuildString_GifPalette() + { + var streamIndex = 0; + var size = new Size(640, 480); + + var str = FFMpegArguments + .FromFileInput("input.mp4") + .OutputToFile("output.gif", false, opt => opt + .WithGifPaletteArgument(streamIndex, size)) + .Arguments; + + Assert.AreEqual($""" + -i "input.mp4" -filter_complex "[0:v] fps=12,scale=w={size.Width}:h={size.Height},split [a][b];[a] palettegen=max_colors=32 [p];[b][p] paletteuse=dither=bayer" "output.gif" + """, str); + } + + [TestMethod] + public void Builder_BuildString_GifPalette_NullSize_FpsSupplied() + { + var streamIndex = 1; + + var str = FFMpegArguments + .FromFileInput("input.mp4") + .OutputToFile("output.gif", false, opt => opt + .WithGifPaletteArgument(streamIndex, null, 10)) + .Arguments; + + Assert.AreEqual($""" + -i "input.mp4" -filter_complex "[{streamIndex}:v] fps=10,split [a][b];[a] palettegen=max_colors=32 [p];[b][p] paletteuse=dither=bayer" "output.gif" + """, str); + } + + [TestMethod] + public void Builder_BuildString_MultiOutput() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .MultiOutput(args => args + .OutputToFile("output.mp4", true, args => args.CopyChannel()) + .OutputToFile("output.ts", false, args => args.CopyChannel().ForceFormat("mpegts")) + .OutputToUrl("http://server/path", options => options.ForceFormat("webm"))) + .Arguments; + Assert.AreEqual("""-i "input.mp4" -c:a copy -c:v copy "output.mp4" -y -c:a copy -c:v copy -f mpegts "output.ts" -f webm http://server/path""", str); + } + + [TestMethod] + public void Builder_BuildString_MBROutput() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .MultiOutput(args => args + .OutputToFile("sd.mp4", true, args => args.Resize(1200, 720)) + .OutputToFile("hd.mp4", false, args => args.Resize(1920, 1080))) + .Arguments; + Assert.AreEqual(""" + -i "input.mp4" -s 1200x720 "sd.mp4" -y -s 1920x1080 "hd.mp4" + """, str); + } + + [TestMethod] + public void Builder_BuildString_TeeOutput() + { + var str = FFMpegArguments.FromFileInput("input.mp4") + .OutputToTee(args => args + .OutputToFile("output.mp4", false, args => args.WithFastStart()) + .OutputToUrl("http://server/path", options => options.ForceFormat("mpegts").SelectStream(0, channel: Channel.Video))) + .Arguments; + Assert.AreEqual(""" + -i "input.mp4" -f tee "[movflags=faststart]output.mp4|[f=mpegts:select=\'0:v:0\']http://server/path" + """, str); + } + + [TestMethod] + public void Builder_BuildString_MultiInput() + { + var audioStreams = string.Join("", _multiFiles.Select((item, index) => $"[{index}:0]")); + var mixFilter = $"{audioStreams}amix=inputs={_multiFiles.Length}:duration=longest:dropout_transition=1:normalize=0[final]"; + var ffmpegArgs = $"-filter_complex \"{mixFilter}\" -map \"[final]\""; + var str = FFMpegArguments + .FromFileInput(_multiFiles) + .OutputToFile("output.mp3", true, options => options + .WithCustomArgument(ffmpegArgs) + .WithAudioCodec(AudioCodec.LibMp3Lame) // Set the audio codec to MP3 + .WithAudioBitrate(128) // Set the bitrate to 128kbps + .WithAudioSamplingRate() // Set the sample rate to 48kHz + .WithoutMetadata() // Remove metadata + .WithCustomArgument("-ac 2 -write_xing 0 -id3v2_version 0")) // Force 2 Channels + .Arguments; + Assert.AreEqual( + "-i \"1.mp3\" -i \"2.mp3\" -i \"3.mp3\" -i \"4.mp3\" -filter_complex \"[0:0][1:0][2:0][3:0]amix=inputs=4:duration=longest:dropout_transition=1:normalize=0[final]\" -map \"[final]\" -c:a libmp3lame -b:a 128k -ar 48000 -map_metadata -1 -ac 2 -write_xing 0 -id3v2_version 0 \"output.mp3\" -y", + str); + } + + [TestMethod] + public void Pre_VerifyExists_AllFilesExist() + { + // Arrange + var filePaths = new List { Path.GetTempFileName(), Path.GetTempFileName(), Path.GetTempFileName() }; + var argument = new MultiInputArgument(true, filePaths); + try { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4").Arguments; - Assert.AreEqual("-i \"input.mp4\" \"output.mp4\" -y", str); - } - - [TestMethod] - public void Builder_BuildString_Scale() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", true, opt => opt - .WithVideoFilters(filterOptions => filterOptions - .Scale(VideoSize.Hd))) - .Arguments; - Assert.AreEqual("-i \"input.mp4\" -vf \"scale=-1:720\" \"output.mp4\" -y", str); - } - - [TestMethod] - public void Builder_BuildString_AudioCodec() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", true, opt => opt.WithAudioCodec(AudioCodec.Aac)).Arguments; - Assert.AreEqual("-i \"input.mp4\" -c:a aac \"output.mp4\" -y", str); - } - - [TestMethod] - public void Builder_BuildString_AudioBitrate() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", true, opt => opt.WithAudioBitrate(AudioQuality.Normal)).Arguments; - Assert.AreEqual("-i \"input.mp4\" -b:a 128k \"output.mp4\" -y", str); - } - - [TestMethod] - public void Builder_BuildString_Quiet() - { - var str = FFMpegArguments.FromFileInput("input.mp4").WithGlobalOptions(opt => opt.WithVerbosityLevel()) - .OutputToFile("output.mp4", false).Arguments; - Assert.AreEqual("-hide_banner -loglevel error -i \"input.mp4\" \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_AudioCodec_Fluent() - { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, - opt => opt.WithAudioCodec(AudioCodec.Aac).WithAudioBitrate(128)).Arguments; - Assert.AreEqual("-i \"input.mp4\" -c:a aac -b:a 128k \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_BitStream() - { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, - opt => opt.WithBitStreamFilter(Channel.Audio, Filter.H264_Mp4ToAnnexB)).Arguments; - Assert.AreEqual("-i \"input.mp4\" -bsf:a h264_mp4toannexb \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_HardwareAcceleration_Auto() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt.WithHardwareAcceleration()).Arguments; - Assert.AreEqual("-i \"input.mp4\" -hwaccel auto \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_HardwareAcceleration_Specific() - { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, - opt => opt.WithHardwareAcceleration(HardwareAccelerationDevice.CUVID)).Arguments; - Assert.AreEqual("-i \"input.mp4\" -hwaccel cuvid \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_Concat() - { - var str = FFMpegArguments.FromConcatInput(_concatFiles).OutputToFile("output.mp4", false).Arguments; - Assert.AreEqual("-i \"concat:1.mp4|2.mp4|3.mp4|4.mp4\" \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_Copy_Audio() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt.CopyChannel(Channel.Audio)).Arguments; - Assert.AreEqual("-i \"input.mp4\" -c:a copy \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_Copy_Video() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt.CopyChannel(Channel.Video)).Arguments; - Assert.AreEqual("-i \"input.mp4\" -c:v copy \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_Copy_Both() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt.CopyChannel()).Arguments; - Assert.AreEqual("-i \"input.mp4\" -c:a copy -c:v copy \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_DisableChannel_Audio() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt.DisableChannel(Channel.Audio)).Arguments; - Assert.AreEqual("-i \"input.mp4\" -an \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_DisableChannel_Video() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt.DisableChannel(Channel.Video)).Arguments; - Assert.AreEqual("-i \"input.mp4\" -vn \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_AudioSamplingRate_Default() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt.WithAudioSamplingRate()).Arguments; - Assert.AreEqual("-i \"input.mp4\" -ar 48000 \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_AudioSamplingRate() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt.WithAudioSamplingRate(44000)).Arguments; - Assert.AreEqual("-i \"input.mp4\" -ar 44000 \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_VariableBitrate() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt.WithVariableBitrate(5)).Arguments; - Assert.AreEqual("-i \"input.mp4\" -vbr 5 \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_Faststart() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt.WithFastStart()).Arguments; - Assert.AreEqual("-i \"input.mp4\" -movflags faststart \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_Overwrite() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt.OverwriteExisting()).Arguments; - Assert.AreEqual("-i \"input.mp4\" -y \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_RemoveMetadata() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt.WithoutMetadata()).Arguments; - Assert.AreEqual("-i \"input.mp4\" -map_metadata -1 \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_Transpose() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt - .WithVideoFilters(filterOptions => filterOptions - .Transpose(Transposition.CounterClockwise90))) - .Arguments; - Assert.AreEqual("-i \"input.mp4\" -vf \"transpose=2\" \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_Mirroring() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt - .WithVideoFilters(filterOptions => filterOptions - .Mirror(Mirroring.Horizontal))) - .Arguments; - Assert.AreEqual("-i \"input.mp4\" -vf \"hflip\" \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_TransposeScale() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt - .WithVideoFilters(filterOptions => filterOptions - .Transpose(Transposition.CounterClockwise90) - .Scale(200, 300))) - .Arguments; - Assert.AreEqual("-i \"input.mp4\" -vf \"transpose=2, scale=200:300\" \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_ForceFormat() - { - var str = FFMpegArguments.FromFileInput("input.mp4", false, opt => opt.ForceFormat(VideoType.Mp4)) - .OutputToFile("output.mp4", false, opt => opt.ForceFormat(VideoType.Mp4)).Arguments; - Assert.AreEqual("-f mp4 -i \"input.mp4\" -f mp4 \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_FrameOutputCount() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt.WithFrameOutputCount(50)).Arguments; - Assert.AreEqual("-i \"input.mp4\" -vframes 50 \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_VideoStreamNumber() - { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.SelectStream(1)).Arguments; - Assert.AreEqual("-i \"input.mp4\" -map 0:1 \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_FrameRate() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt.WithFramerate(50)).Arguments; - Assert.AreEqual("-i \"input.mp4\" -r 50 \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_Loop() - { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", false, opt => opt.Loop(50)) - .Arguments; - Assert.AreEqual("-i \"input.mp4\" -loop 50 \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_Seek() - { - var str = FFMpegArguments.FromFileInput("input.mp4", false, opt => opt.Seek(TimeSpan.FromSeconds(10))).OutputToFile("output.mp4", false, opt => opt.Seek(TimeSpan.FromSeconds(10))).Arguments; - Assert.AreEqual("-ss 00:00:10.000 -i \"input.mp4\" -ss 00:00:10.000 \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_EndSeek() - { - var str = FFMpegArguments.FromFileInput("input.mp4", false, opt => opt.EndSeek(TimeSpan.FromSeconds(10))).OutputToFile("output.mp4", false, opt => opt.EndSeek(TimeSpan.FromSeconds(10))).Arguments; - Assert.AreEqual("-to 00:00:10.000 -i \"input.mp4\" -to 00:00:10.000 \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_Shortest() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt.UsingShortest()).Arguments; - Assert.AreEqual("-i \"input.mp4\" -shortest \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_Size() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt.Resize(1920, 1080)).Arguments; - Assert.AreEqual("-i \"input.mp4\" -s 1920x1080 \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_Speed() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt.WithSpeedPreset(Speed.Fast)).Arguments; - Assert.AreEqual("-i \"input.mp4\" -preset fast \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_DrawtextFilter() - { - var str = FFMpegArguments - .FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt - .WithVideoFilters(filterOptions => filterOptions - .DrawText(DrawTextOptions - .Create("Stack Overflow", "/path/to/font.ttf") - .WithParameter("fontcolor", "white") - .WithParameter("fontsize", "24") - .WithParameter("box", "1") - .WithParameter("boxcolor", "black@0.5") - .WithParameter("boxborderw", "5") - .WithParameter("x", "(w-text_w)/2") - .WithParameter("y", "(h-text_h)/2")))) - .Arguments; - - Assert.AreEqual( - "-i \"input.mp4\" -vf \"drawtext=text='Stack Overflow':fontfile=/path/to/font.ttf:fontcolor=white:fontsize=24:box=1:boxcolor=black@0.5:boxborderw=5:x=(w-text_w)/2:y=(h-text_h)/2\" \"output.mp4\"", - str); - } - - [TestMethod] - public void Builder_BuildString_DrawtextFilter_Alt() - { - var str = FFMpegArguments - .FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt - .WithVideoFilters(filterOptions => filterOptions - .DrawText(DrawTextOptions - .Create("Stack Overflow", "/path/to/font.ttf", ("fontcolor", "white"), ("fontsize", "24"))))) - .Arguments; - - Assert.AreEqual( - "-i \"input.mp4\" -vf \"drawtext=text='Stack Overflow':fontfile=/path/to/font.ttf:fontcolor=white:fontsize=24\" \"output.mp4\"", - str); - } - - [TestMethod] - public void Builder_BuildString_SubtitleHardBurnFilter() - { - var str = FFMpegArguments - .FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt - .WithVideoFilters(filterOptions => filterOptions - .HardBurnSubtitle(SubtitleHardBurnOptions - .Create(subtitlePath: "sample.srt") - .SetCharacterEncoding("UTF-8") - .SetOriginalSize(1366, 768) - .SetSubtitleIndex(0) - .WithStyle(StyleOptions.Create() - .WithParameter("FontName", "DejaVu Serif") - .WithParameter("PrimaryColour", "&HAA00FF00"))))) - .Arguments; - - Assert.AreEqual("-i \"input.mp4\" -vf \"subtitles='sample.srt':charenc=UTF-8:original_size=1366x768:stream_index=0:force_style='FontName=DejaVu Serif\\,PrimaryColour=&HAA00FF00'\" \"output.mp4\"", - str); - } - - [TestMethod] - public void Builder_BuildString_SubtitleHardBurnFilterFixedPaths() - { - var str = FFMpegArguments - .FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt - .WithVideoFilters(filterOptions => filterOptions - .HardBurnSubtitle(SubtitleHardBurnOptions - .Create(subtitlePath: @"sample( \ : [ ] , ' ).srt")))) - .Arguments; - - Assert.AreEqual(@"-i ""input.mp4"" -vf ""subtitles='sample( \\ \: \[ \] \, '\\\'' ).srt'"" ""output.mp4""", - str); - } - - [TestMethod] - public void Builder_BuildString_StartNumber() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt.WithStartNumber(50)).Arguments; - Assert.AreEqual("-i \"input.mp4\" -start_number 50 \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_Threads_1() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt.UsingThreads(50)).Arguments; - Assert.AreEqual("-i \"input.mp4\" -threads 50 \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_Threads_2() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt.UsingMultithreading(true)).Arguments; - Assert.AreEqual($"-i \"input.mp4\" -threads {Environment.ProcessorCount} \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_Codec() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt.WithVideoCodec(VideoCodec.LibX264)).Arguments; - Assert.AreEqual("-i \"input.mp4\" -c:v libx264 \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_Codec_Override() - { - var str = FFMpegArguments.FromFileInput("input.mp4").OutputToFile("output.mp4", true, - opt => opt.WithVideoCodec(VideoCodec.LibX264).ForcePixelFormat("yuv420p")).Arguments; - Assert.AreEqual("-i \"input.mp4\" -c:v libx264 -pix_fmt yuv420p \"output.mp4\" -y", str); - } - - [TestMethod] - public void Builder_BuildString_Duration() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt.WithDuration(TimeSpan.FromSeconds(20))).Arguments; - Assert.AreEqual("-i \"input.mp4\" -t 00:00:20 \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_Raw() - { - var str = FFMpegArguments.FromFileInput("input.mp4", false, opt => opt.WithCustomArgument(null!)) - .OutputToFile("output.mp4", false, opt => opt.WithCustomArgument(null!)).Arguments; - Assert.AreEqual(" -i \"input.mp4\" \"output.mp4\"", str); - - str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt.WithCustomArgument("-acodec copy")).Arguments; - Assert.AreEqual("-i \"input.mp4\" -acodec copy \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_ForcePixelFormat() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt.ForcePixelFormat("yuv444p")).Arguments; - Assert.AreEqual("-i \"input.mp4\" -pix_fmt yuv444p \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_PanAudioFilterChannelNumber() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, - opt => opt.WithAudioFilters(filterOptions => filterOptions.Pan(2, "c0=c1", "c1=c1"))) - .Arguments; - - Assert.AreEqual("-i \"input.mp4\" -af \"pan=2c|c0=c1|c1=c1\" \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_PanAudioFilterChannelLayout() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, - opt => opt.WithAudioFilters(filterOptions => filterOptions.Pan("stereo", "c0=c0", "c1=c1"))) - .Arguments; - - Assert.AreEqual("-i \"input.mp4\" -af \"pan=stereo|c0=c0|c1=c1\" \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_PanAudioFilterChannelNoOutputDefinition() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, - opt => opt.WithAudioFilters(filterOptions => filterOptions.Pan("stereo"))) - .Arguments; - - Assert.AreEqual("-i \"input.mp4\" -af \"pan=stereo\" \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_DynamicAudioNormalizerDefaultFormat() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, - opt => opt.WithAudioFilters(filterOptions => filterOptions.DynamicNormalizer())) - .Arguments; - - Assert.AreEqual("-i \"input.mp4\" -af \"dynaudnorm=f=500:g=31:p=0.95:m=10.0:r=0.0:n=1:c=0:b=0:s=0.0\" \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_DynamicAudioNormalizerWithValuesFormat() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, - opt => opt.WithAudioFilters(filterOptions => filterOptions.DynamicNormalizer(125, 13, 0.9215, 5.124, 0.5458, false, true, true, 0.3333333))) - .Arguments; - - Assert.AreEqual("-i \"input.mp4\" -af \"dynaudnorm=f=125:g=13:p=0.92:m=5.1:r=0.5:n=0:c=1:b=1:s=0.3\" \"output.mp4\"", str); - } - - [TestMethod] - public void Builder_BuildString_Audible_AAXC_Decryption() - { - var str = FFMpegArguments.FromFileInput("input.aaxc", false, x => x.WithAudibleEncryptionKeys("123", "456")) - .MapMetaData() - .OutputToFile("output.m4b", true, x => x.WithTagVersion(3).DisableChannel(Channel.Video).CopyChannel(Channel.Audio)) - .Arguments; - - Assert.AreEqual("-audible_key 123 -audible_iv 456 -i \"input.aaxc\" -map_metadata 0 -id3v2_version 3 -vn -c:a copy \"output.m4b\" -y", str); - } - - [TestMethod] - public void Builder_BuildString_PadFilter() - { - var str = FFMpegArguments - .FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt - .WithVideoFilters(filterOptions => filterOptions - .Pad(PadOptions - .Create("max(iw,ih)", "ow") - .WithParameter("x", "(ow-iw)/2") - .WithParameter("y", "(oh-ih)/2") - .WithParameter("color", "violet") - .WithParameter("eval", "frame")))) - .Arguments; - - Assert.AreEqual( - "-i \"input.mp4\" -vf \"pad=width=max(iw\\,ih):height=ow:x=(ow-iw)/2:y=(oh-ih)/2:color=violet:eval=frame\" \"output.mp4\"", - str); - } - - [TestMethod] - public void Builder_BuildString_PadFilter_Alt() - { - var str = FFMpegArguments - .FromFileInput("input.mp4") - .OutputToFile("output.mp4", false, opt => opt - .WithVideoFilters(filterOptions => filterOptions - .Pad(PadOptions - .Create("4/3") - .WithParameter("x", "(ow-iw)/2") - .WithParameter("y", "(oh-ih)/2") - .WithParameter("color", "violet") - .WithParameter("eval", "frame")))) - .Arguments; - - Assert.AreEqual( - "-i \"input.mp4\" -vf \"pad=aspect=4/3:x=(ow-iw)/2:y=(oh-ih)/2:color=violet:eval=frame\" \"output.mp4\"", - str); - } - - [TestMethod] - public void Builder_BuildString_GifPalette() - { - var streamIndex = 0; - var size = new Size(640, 480); - - var str = FFMpegArguments - .FromFileInput("input.mp4") - .OutputToFile("output.gif", false, opt => opt - .WithGifPaletteArgument(streamIndex, size)) - .Arguments; - - Assert.AreEqual($""" - -i "input.mp4" -filter_complex "[0:v] fps=12,scale=w={size.Width}:h={size.Height},split [a][b];[a] palettegen=max_colors=32 [p];[b][p] paletteuse=dither=bayer" "output.gif" - """, str); - } - - [TestMethod] - public void Builder_BuildString_GifPalette_NullSize_FpsSupplied() - { - var streamIndex = 1; - - var str = FFMpegArguments - .FromFileInput("input.mp4") - .OutputToFile("output.gif", false, opt => opt - .WithGifPaletteArgument(streamIndex, null, 10)) - .Arguments; - - Assert.AreEqual($""" - -i "input.mp4" -filter_complex "[{streamIndex}:v] fps=10,split [a][b];[a] palettegen=max_colors=32 [p];[b][p] paletteuse=dither=bayer" "output.gif" - """, str); - } - - [TestMethod] - public void Builder_BuildString_MultiOutput() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .MultiOutput(args => args - .OutputToFile("output.mp4", overwrite: true, args => args.CopyChannel()) - .OutputToFile("output.ts", overwrite: false, args => args.CopyChannel().ForceFormat("mpegts")) - .OutputToUrl("http://server/path", options => options.ForceFormat("webm"))) - .Arguments; - Assert.AreEqual($""" - -i "input.mp4" -c:a copy -c:v copy "output.mp4" -y -c:a copy -c:v copy -f mpegts "output.ts" -f webm http://server/path - """, str); - } - - [TestMethod] - public void Builder_BuildString_MBROutput() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .MultiOutput(args => args - .OutputToFile("sd.mp4", overwrite: true, args => args.Resize(1200, 720)) - .OutputToFile("hd.mp4", overwrite: false, args => args.Resize(1920, 1080))) - .Arguments; - Assert.AreEqual($""" - -i "input.mp4" -s 1200x720 "sd.mp4" -y -s 1920x1080 "hd.mp4" - """, str); - } - - [TestMethod] - public void Builder_BuildString_TeeOutput() - { - var str = FFMpegArguments.FromFileInput("input.mp4") - .OutputToTee(args => args - .OutputToFile("output.mp4", overwrite: false, args => args.WithFastStart()) - .OutputToUrl("http://server/path", options => options.ForceFormat("mpegts").SelectStream(0, channel: Channel.Video))) - .Arguments; - Assert.AreEqual($""" - -i "input.mp4" -f tee "[movflags=faststart]output.mp4|[f=mpegts:select=\'0:v:0\']http://server/path" - """, str); - } - [TestMethod] - public void Builder_BuildString_MultiInput() - { - var audioStreams = string.Join("", _multiFiles.Select((item, index) => $"[{index}:0]")); - var mixFilter = $"{audioStreams}amix=inputs={_multiFiles.Length}:duration=longest:dropout_transition=1:normalize=0[final]"; - var ffmpegArgs = $"-filter_complex \"{mixFilter}\" -map \"[final]\""; - var str = FFMpegArguments - .FromFileInput(_multiFiles) - .OutputToFile("output.mp3", overwrite: true, options => options - .WithCustomArgument(ffmpegArgs) - .WithAudioCodec(AudioCodec.LibMp3Lame) // Set the audio codec to MP3 - .WithAudioBitrate(128) // Set the bitrate to 128kbps - .WithAudioSamplingRate(48000) // Set the sample rate to 48kHz - .WithoutMetadata() // Remove metadata - .WithCustomArgument("-ac 2 -write_xing 0 -id3v2_version 0")) // Force 2 Channels - .Arguments; - Assert.AreEqual($"-i \"1.mp3\" -i \"2.mp3\" -i \"3.mp3\" -i \"4.mp3\" -filter_complex \"[0:0][1:0][2:0][3:0]amix=inputs=4:duration=longest:dropout_transition=1:normalize=0[final]\" -map \"[final]\" -c:a libmp3lame -b:a 128k -ar 48000 -map_metadata -1 -ac 2 -write_xing 0 -id3v2_version 0 \"output.mp3\" -y", str); - } - [TestMethod] - public void Pre_VerifyExists_AllFilesExist() - { - // Arrange - var filePaths = new List - { - Path.GetTempFileName(), - Path.GetTempFileName(), - Path.GetTempFileName() - }; - var argument = new MultiInputArgument(true, filePaths); - try - { - // Act & Assert - argument.Pre(); // No exception should be thrown - } - finally - { - // Cleanup - foreach (var filePath in filePaths) - { - File.Delete(filePath); - } - } - } - - [TestMethod] - public void Pre_VerifyExists_SomeFilesNotExist() - { - // Arrange - var filePaths = new List - { - Path.GetTempFileName(), - "file2.mp4", - "file3.mp4" - }; - var argument = new MultiInputArgument(true, filePaths); - try - { - // Act & Assert - Assert.ThrowsException(() => argument.Pre()); - } - finally - { - // Cleanup - File.Delete(filePaths[0]); - } - } - - [TestMethod] - public void Pre_VerifyExists_NoFilesExist() - { - // Arrange - var filePaths = new List - { - "file1.mp4", - "file2.mp4", - "file3.mp4" - }; - var argument = new MultiInputArgument(true, filePaths); // Act & Assert - Assert.ThrowsException(() => argument.Pre()); + argument.Pre(); // No exception should be thrown + } + finally + { + // Cleanup + foreach (var filePath in filePaths) + { + File.Delete(filePath); + } } } + + [TestMethod] + public void Pre_VerifyExists_SomeFilesNotExist() + { + // Arrange + var filePaths = new List { Path.GetTempFileName(), "file2.mp4", "file3.mp4" }; + var argument = new MultiInputArgument(true, filePaths); + try + { + // Act & Assert + Assert.ThrowsExactly(() => argument.Pre()); + } + finally + { + // Cleanup + File.Delete(filePaths[0]); + } + } + + [TestMethod] + public void Pre_VerifyExists_NoFilesExist() + { + // Arrange + var filePaths = new List { "file1.mp4", "file2.mp4", "file3.mp4" }; + var argument = new MultiInputArgument(true, filePaths); + // Act & Assert + Assert.ThrowsExactly(() => argument.Pre()); + } + + [TestMethod] + public void Concat_Escape() + { + var arg = new DemuxConcatArgument([@"Heaven's River\05 - Investigation.m4b"]); + CollectionAssert.AreEquivalent(new[] { @"file 'Heaven'\''s River\05 - Investigation.m4b'" }, arg.Values.ToArray()); + } + + [TestMethod] + public void Audible_Aaxc_Test() + { + var arg = new AudibleEncryptionKeyArgument("123", "456"); + Assert.AreEqual("-audible_key 123 -audible_iv 456", arg.Text); + } + + [TestMethod] + public void Audible_Aax_Test() + { + var arg = new AudibleEncryptionKeyArgument("62689101"); + Assert.AreEqual("-activation_bytes 62689101", arg.Text); + } + + [TestMethod] + public void InputPipe_MaxLength_ShorterThanMacOSMax() + { + var pipePath = new InputPipeArgument(new StreamPipeSource(Stream.Null)).PipePath; + Assert.IsLessThan(104, pipePath.Length); + } + + [TestMethod] + public void OutputPipe_MaxLength_ShorterThanMacOSMax() + { + var pipePath = new OutputPipeArgument(new StreamPipeSink(Stream.Null)).PipePath; + Assert.IsLessThan(_macOsMaxPipePathLength, pipePath.Length); + } } diff --git a/FFMpegCore.Test/Assembly.cs b/FFMpegCore.Test/Assembly.cs new file mode 100644 index 0000000..aaf278c --- /dev/null +++ b/FFMpegCore.Test/Assembly.cs @@ -0,0 +1 @@ +[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)] diff --git a/FFMpegCore.Test/AudioTest.cs b/FFMpegCore.Test/AudioTest.cs index ba4e3eb..3960b9f 100644 --- a/FFMpegCore.Test/AudioTest.cs +++ b/FFMpegCore.Test/AudioTest.cs @@ -3,326 +3,297 @@ using FFMpegCore.Exceptions; using FFMpegCore.Extend; using FFMpegCore.Pipes; using FFMpegCore.Test.Resources; -using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace FFMpegCore.Test +namespace FFMpegCore.Test; + +[TestClass] +public class AudioTest { - [TestClass] - public class AudioTest + [TestMethod] + public void Audio_Remove() { - [TestMethod] - public void Audio_Remove() - { - using var outputFile = new TemporaryFile("out.mp4"); + using var outputFile = new TemporaryFile("out.mp4"); - FFMpeg.Mute(TestResources.Mp4Video, outputFile); - var analysis = FFProbe.Analyse(outputFile); + FFMpeg.Mute(TestResources.Mp4Video, outputFile); + var analysis = FFProbe.Analyse(outputFile); - Assert.IsTrue(analysis.VideoStreams.Any()); - Assert.IsTrue(!analysis.AudioStreams.Any()); - } + Assert.IsNotEmpty(analysis.VideoStreams); + Assert.IsEmpty(analysis.AudioStreams); + } - [TestMethod] - public void Audio_Save() - { - using var outputFile = new TemporaryFile("out.mp3"); + [TestMethod] + public void Audio_Save() + { + using var outputFile = new TemporaryFile("out.mp3"); - FFMpeg.ExtractAudio(TestResources.Mp4Video, outputFile); - var analysis = FFProbe.Analyse(outputFile); + FFMpeg.ExtractAudio(TestResources.Mp4Video, outputFile); + var analysis = FFProbe.Analyse(outputFile); - Assert.IsTrue(!analysis.VideoStreams.Any()); - Assert.IsTrue(analysis.AudioStreams.Any()); - } - [TestMethod] - public async Task Audio_FromRaw() - { - await using var file = File.Open(TestResources.RawAudio, FileMode.Open); - var memoryStream = new MemoryStream(); - await FFMpegArguments - .FromPipeInput(new StreamPipeSource(file), options => options.ForceFormat("s16le")) - .OutputToPipe(new StreamPipeSink(memoryStream), options => options.ForceFormat("mp3")) - .ProcessAsynchronously(); - } + Assert.IsNotEmpty(analysis.AudioStreams); + Assert.IsEmpty(analysis.VideoStreams); + } - [TestMethod] - public void Audio_Add() - { - using var outputFile = new TemporaryFile("out.mp4"); + [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(); + } - var success = FFMpeg.ReplaceAudio(TestResources.Mp4WithoutAudio, TestResources.Mp3Audio, outputFile); - var videoAnalysis = FFProbe.Analyse(TestResources.Mp4WithoutAudio); - var audioAnalysis = FFProbe.Analyse(TestResources.Mp3Audio); - var outputAnalysis = FFProbe.Analyse(outputFile); + [TestMethod] + public void Audio_Add() + { + using var outputFile = new TemporaryFile("out.mp4"); - Assert.IsTrue(success); - Assert.AreEqual(Math.Max(videoAnalysis.Duration.TotalSeconds, audioAnalysis.Duration.TotalSeconds), outputAnalysis.Duration.TotalSeconds, 0.15); - Assert.IsTrue(File.Exists(outputFile)); - } + var success = FFMpeg.ReplaceAudio(TestResources.Mp4WithoutAudio, TestResources.Mp3Audio, outputFile); + var videoAnalysis = FFProbe.Analyse(TestResources.Mp4WithoutAudio); + var audioAnalysis = FFProbe.Analyse(TestResources.Mp3Audio); + var outputAnalysis = FFProbe.Analyse(outputFile); - [TestMethod] - public void Image_AddAudio() - { - 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)); - } + Assert.IsTrue(success); + Assert.AreEqual(Math.Max(videoAnalysis.Duration.TotalSeconds, audioAnalysis.Duration.TotalSeconds), outputAnalysis.Duration.TotalSeconds, 0.15); + Assert.IsTrue(File.Exists(outputFile)); + } - [TestMethod, Timeout(10000)] - public void Audio_ToAAC_Args_Pipe() - { - using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + [TestMethod] + public void Image_AddAudio() + { + 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 - { - new PcmAudioSampleWrapper(new byte[] { 0, 0 }), - new PcmAudioSampleWrapper(new byte[] { 0, 0 }), - }; + [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] + public void Audio_ToAAC_Args_Pipe() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); - var audioSamplesSource = new RawAudioPipeSource(samples) - { - Channels = 2, - Format = "s8", - SampleRate = 8000, - }; + var samples = new List { new PcmAudioSampleWrapper([0, 0]), new PcmAudioSampleWrapper([0, 0]) }; - var success = FFMpegArguments - .FromPipeInput(audioSamplesSource) - .OutputToFile(outputFile, false, opt => opt - .WithAudioCodec(AudioCodec.Aac)) - .ProcessSynchronously(); - Assert.IsTrue(success); - } + var audioSamplesSource = new RawAudioPipeSource(samples) { Channels = 2, Format = "s8", SampleRate = 8000 }; - [TestMethod, Timeout(10000)] - public void Audio_ToLibVorbis_Args_Pipe() - { - using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + var success = FFMpegArguments + .FromPipeInput(audioSamplesSource) + .OutputToFile(outputFile, false, opt => opt + .WithAudioCodec(AudioCodec.Aac)) + .ProcessSynchronously(); + Assert.IsTrue(success); + } - var samples = new List - { - new PcmAudioSampleWrapper(new byte[] { 0, 0 }), - new PcmAudioSampleWrapper(new byte[] { 0, 0 }), - }; + [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] + public void Audio_ToLibVorbis_Args_Pipe() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); - var audioSamplesSource = new RawAudioPipeSource(samples) - { - Channels = 2, - Format = "s8", - SampleRate = 8000, - }; + var samples = new List { new PcmAudioSampleWrapper([0, 0]), new PcmAudioSampleWrapper([0, 0]) }; - var success = FFMpegArguments - .FromPipeInput(audioSamplesSource) - .OutputToFile(outputFile, false, opt => opt - .WithAudioCodec(AudioCodec.LibVorbis)) - .ProcessSynchronously(); - Assert.IsTrue(success); - } + var audioSamplesSource = new RawAudioPipeSource(samples) { Channels = 2, Format = "s8", SampleRate = 8000 }; - [TestMethod, Timeout(10000)] - public async Task Audio_ToAAC_Args_Pipe_Async() - { - using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + var success = FFMpegArguments + .FromPipeInput(audioSamplesSource) + .OutputToFile(outputFile, false, opt => opt + .WithAudioCodec(AudioCodec.LibVorbis)) + .ProcessSynchronously(); + Assert.IsTrue(success); + } - var samples = new List - { - new PcmAudioSampleWrapper(new byte[] { 0, 0 }), - new PcmAudioSampleWrapper(new byte[] { 0, 0 }), - }; + [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] + public async Task Audio_ToAAC_Args_Pipe_Async() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); - var audioSamplesSource = new RawAudioPipeSource(samples) - { - Channels = 2, - Format = "s8", - SampleRate = 8000, - }; + var samples = new List { new PcmAudioSampleWrapper([0, 0]), new PcmAudioSampleWrapper([0, 0]) }; - var success = await FFMpegArguments - .FromPipeInput(audioSamplesSource) - .OutputToFile(outputFile, false, opt => opt - .WithAudioCodec(AudioCodec.Aac)) - .ProcessAsynchronously(); - Assert.IsTrue(success); - } + var audioSamplesSource = new RawAudioPipeSource(samples) { Channels = 2, Format = "s8", SampleRate = 8000 }; - [TestMethod, Timeout(10000)] - public void Audio_ToAAC_Args_Pipe_ValidDefaultConfiguration() - { - using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + var success = await FFMpegArguments + .FromPipeInput(audioSamplesSource) + .OutputToFile(outputFile, false, opt => opt + .WithAudioCodec(AudioCodec.Aac)) + .ProcessAsynchronously(); + Assert.IsTrue(success); + } - var samples = new List - { - new PcmAudioSampleWrapper(new byte[] { 0, 0 }), - new PcmAudioSampleWrapper(new byte[] { 0, 0 }), - }; + [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] + public void Audio_ToAAC_Args_Pipe_ValidDefaultConfiguration() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); - var audioSamplesSource = new RawAudioPipeSource(samples); + var samples = new List { new PcmAudioSampleWrapper([0, 0]), new PcmAudioSampleWrapper([0, 0]) }; - var success = FFMpegArguments - .FromPipeInput(audioSamplesSource) - .OutputToFile(outputFile, false, opt => opt - .WithAudioCodec(AudioCodec.Aac)) - .ProcessSynchronously(); - Assert.IsTrue(success); - } + var audioSamplesSource = new RawAudioPipeSource(samples); - [TestMethod, Timeout(10000)] - public void Audio_ToAAC_Args_Pipe_InvalidChannels() - { - using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + var success = FFMpegArguments + .FromPipeInput(audioSamplesSource) + .OutputToFile(outputFile, false, opt => opt + .WithAudioCodec(AudioCodec.Aac)) + .ProcessSynchronously(); + Assert.IsTrue(success); + } - var audioSamplesSource = new RawAudioPipeSource(new List()) - { - Channels = 0, - }; + [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] + public void Audio_ToAAC_Args_Pipe_InvalidChannels() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); - var ex = Assert.ThrowsException(() => FFMpegArguments - .FromPipeInput(audioSamplesSource) - .OutputToFile(outputFile, false, opt => opt - .WithAudioCodec(AudioCodec.Aac)) - .ProcessSynchronously()); - } + var audioSamplesSource = new RawAudioPipeSource(new List()) { Channels = 0 }; - [TestMethod, Timeout(10000)] - public void Audio_ToAAC_Args_Pipe_InvalidFormat() - { - using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + Assert.ThrowsExactly(() => FFMpegArguments + .FromPipeInput(audioSamplesSource) + .OutputToFile(outputFile, false, opt => opt + .WithAudioCodec(AudioCodec.Aac)) + .ProcessSynchronously()); + } - var audioSamplesSource = new RawAudioPipeSource(new List()) - { - Format = "s8le", - }; + [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] + public void Audio_ToAAC_Args_Pipe_InvalidFormat() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); - var ex = Assert.ThrowsException(() => FFMpegArguments - .FromPipeInput(audioSamplesSource) - .OutputToFile(outputFile, false, opt => opt - .WithAudioCodec(AudioCodec.Aac)) - .ProcessSynchronously()); - } + var audioSamplesSource = new RawAudioPipeSource(new List()) { Format = "s8le" }; - [TestMethod, Timeout(10000)] - public void Audio_ToAAC_Args_Pipe_InvalidSampleRate() - { - using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + Assert.ThrowsExactly(() => FFMpegArguments + .FromPipeInput(audioSamplesSource) + .OutputToFile(outputFile, false, opt => opt + .WithAudioCodec(AudioCodec.Aac)) + .ProcessSynchronously()); + } - var audioSamplesSource = new RawAudioPipeSource(new List()) - { - SampleRate = 0, - }; + [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] + public void Audio_ToAAC_Args_Pipe_InvalidSampleRate() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); - var ex = Assert.ThrowsException(() => FFMpegArguments - .FromPipeInput(audioSamplesSource) - .OutputToFile(outputFile, false, opt => opt - .WithAudioCodec(AudioCodec.Aac)) - .ProcessSynchronously()); - } + var audioSamplesSource = new RawAudioPipeSource(new List()) { SampleRate = 0 }; - [TestMethod, Timeout(10000)] - public void Audio_Pan_ToMono() - { - using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + Assert.ThrowsExactly(() => FFMpegArguments + .FromPipeInput(audioSamplesSource) + .OutputToFile(outputFile, false, opt => opt + .WithAudioCodec(AudioCodec.Aac)) + .ProcessSynchronously()); + } - var success = FFMpegArguments.FromFileInput(TestResources.Mp3Audio) - .OutputToFile(outputFile, true, - argumentOptions => argumentOptions - .WithAudioFilters(filter => filter.Pan(1, "c0 < 0.9 * c0 + 0.1 * c1"))) - .ProcessSynchronously(); + [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] + public void Audio_Pan_ToMono() + { + 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); - Assert.AreEqual(1, mediaAnalysis.AudioStreams.Count); - Assert.AreEqual("mono", mediaAnalysis.PrimaryAudioStream!.ChannelLayout); - } + var mediaAnalysis = FFProbe.Analyse(outputFile); - [TestMethod, Timeout(10000)] - public void Audio_Pan_ToMonoNoDefinitions() - { - using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + Assert.IsTrue(success); + Assert.HasCount(1, mediaAnalysis.AudioStreams); + Assert.AreEqual("mono", mediaAnalysis.PrimaryAudioStream!.ChannelLayout); + } - var success = FFMpegArguments.FromFileInput(TestResources.Mp3Audio) - .OutputToFile(outputFile, true, - argumentOptions => argumentOptions - .WithAudioFilters(filter => filter.Pan(1))) - .ProcessSynchronously(); + [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] + public void Audio_Pan_ToMonoNoDefinitions() + { + 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); - Assert.AreEqual(1, mediaAnalysis.AudioStreams.Count); - Assert.AreEqual("mono", mediaAnalysis.PrimaryAudioStream!.ChannelLayout); - } + var mediaAnalysis = FFProbe.Analyse(outputFile); - [TestMethod, Timeout(10000)] - public void Audio_Pan_ToMonoChannelsToOutputDefinitionsMismatch() - { - using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + Assert.IsTrue(success); + Assert.HasCount(1, mediaAnalysis.AudioStreams); + Assert.AreEqual("mono", mediaAnalysis.PrimaryAudioStream!.ChannelLayout); + } - var ex = Assert.ThrowsException(() => FFMpegArguments.FromFileInput(TestResources.Mp3Audio) - .OutputToFile(outputFile, true, - argumentOptions => argumentOptions - .WithAudioFilters(filter => filter.Pan(1, "c0=c0", "c1=c1"))) - .ProcessSynchronously()); - } + [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] + public void Audio_Pan_ToMonoChannelsToOutputDefinitionsMismatch() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); - [TestMethod, Timeout(10000)] - public void Audio_Pan_ToMonoChannelsLayoutToOutputDefinitionsMismatch() - { - using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + Assert.ThrowsExactly(() => FFMpegArguments.FromFileInput(TestResources.Mp3Audio) + .OutputToFile(outputFile, true, + argumentOptions => argumentOptions + .WithAudioFilters(filter => filter.Pan(1, "c0=c0", "c1=c1"))) + .ProcessSynchronously()); + } - var ex = Assert.ThrowsException(() => FFMpegArguments.FromFileInput(TestResources.Mp3Audio) - .OutputToFile(outputFile, true, - argumentOptions => argumentOptions - .WithAudioFilters(filter => filter.Pan("mono", "c0=c0", "c1=c1"))) - .ProcessSynchronously()); - } + [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] + public void Audio_Pan_ToMonoChannelsLayoutToOutputDefinitionsMismatch() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); - [TestMethod, Timeout(10000)] - public void Audio_DynamicNormalizer_WithDefaultValues() - { - using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + Assert.ThrowsExactly(() => FFMpegArguments.FromFileInput(TestResources.Mp3Audio) + .OutputToFile(outputFile, true, + argumentOptions => argumentOptions + .WithAudioFilters(filter => filter.Pan("mono", "c0=c0", "c1=c1"))) + .ProcessSynchronously()); + } - var success = FFMpegArguments.FromFileInput(TestResources.Mp3Audio) - .OutputToFile(outputFile, true, - argumentOptions => argumentOptions - .WithAudioFilters(filter => filter.DynamicNormalizer())) - .ProcessSynchronously(); + [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] + public void Audio_DynamicNormalizer_WithDefaultValues() + { + 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)] - public void Audio_DynamicNormalizer_WithNonDefaultValues() - { - 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(250, 7, 0.9, 2, 1, false, true, true, 0.5))) - .ProcessSynchronously(); + [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] + public void Audio_DynamicNormalizer_WithNonDefaultValues() + { + 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(250, 7, 0.9, 2, 1, false, true, true, 0.5))) + .ProcessSynchronously(); - [DataTestMethod, Timeout(10000)] - [DataRow(2)] - [DataRow(32)] - [DataRow(8)] - public void Audio_DynamicNormalizer_FilterWindow(int filterWindow) - { - using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + Assert.IsTrue(success); + } - var ex = Assert.ThrowsException(() => FFMpegArguments - .FromFileInput(TestResources.Mp3Audio) - .OutputToFile(outputFile, true, - argumentOptions => argumentOptions - .WithAudioFilters( - filter => filter.DynamicNormalizer(filterWindow: filterWindow))) - .ProcessSynchronously()); - } + [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] + [DataRow(2)] + [DataRow(32)] + [DataRow(8)] + public void Audio_DynamicNormalizer_FilterWindow(int filterWindow) + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + Assert.ThrowsExactly(() => FFMpegArguments + .FromFileInput(TestResources.Mp3Audio) + .OutputToFile(outputFile, true, + argumentOptions => argumentOptions + .WithAudioFilters(filter => filter.DynamicNormalizer(filterWindow: filterWindow))) + .ProcessSynchronously()); } } diff --git a/FFMpegCore.Test/DownloaderTests.cs b/FFMpegCore.Test/DownloaderTests.cs new file mode 100644 index 0000000..d574230 --- /dev/null +++ b/FFMpegCore.Test/DownloaderTests.cs @@ -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); + } + } +} diff --git a/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs b/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs index bb7071d..5631e1d 100644 --- a/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs +++ b/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs @@ -1,103 +1,98 @@ -using System.Reflection; -using FFMpegCore.Arguments; -using FluentAssertions; -using Microsoft.VisualStudio.TestTools.UnitTesting; +namespace FFMpegCore.Test; -namespace FFMpegCore.Test +[TestClass] +public class FFMpegArgumentProcessorTest { - [TestClass] - public class FFMpegArgumentProcessorTest + private static FFMpegArgumentProcessor CreateArgumentProcessor() { - [TestCleanup] - public void TestInitialize() + return FFMpegArguments + .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 }); - 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); - options.WorkingDirectory.Should().Be(sessionWorkingDir); + Assert.AreEqual(globalWorkingDir, options.WorkingDirectory); } - - [TestMethod] - public void Processor_Options_CanBeOverridden_And_Configured() + finally { - var globalConfig = "Whatever"; - GlobalFFOptions.Configure(new FFOptions { WorkingDirectory = globalConfig, TemporaryFilesFolder = globalConfig, BinaryFolder = globalConfig }); + GlobalFFOptions.Configure(new FFOptions()); + } + } + [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 sessionTempDir = "./CurrentRunWorkingDir"; 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); - options.Should().BeEquivalentTo(overrideOptions); - options.TemporaryFilesFolder.Should().BeEquivalentTo(sessionTempDir); - options.BinaryFolder.Should().NotBeEquivalentTo(globalConfig); + Assert.AreEqual(options.WorkingDirectory, overrideOptions.WorkingDirectory); + Assert.AreEqual(options.TemporaryFilesFolder, overrideOptions.TemporaryFilesFolder); + Assert.AreEqual(options.BinaryFolder, overrideOptions.BinaryFolder); + + Assert.AreEqual(sessionTempDir, options.TemporaryFilesFolder); + Assert.AreNotEqual(globalConfig, options.BinaryFolder); } - - [TestMethod] - public void Options_Global_And_Session_Options_Can_Differ() + finally { - var globalWorkingDir = "Whatever"; - GlobalFFOptions.Configure(new FFOptions { WorkingDirectory = globalWorkingDir }); + GlobalFFOptions.Configure(new FFOptions()); + } + } + [TestMethod] + public void ZZZ_Options_Global_And_Session_Options_Can_Differ() + { + var globalWorkingDir = "Whatever"; + + try + { var processor1 = CreateArgumentProcessor(); var sessionWorkingDir = "./CurrentRunWorkingDir"; processor1.Configure(options => options.WorkingDirectory = sessionWorkingDir); var options1 = processor1.GetConfiguredOptions(null); - options1.WorkingDirectory.Should().Be(sessionWorkingDir); + Assert.AreEqual(sessionWorkingDir, options1.WorkingDirectory); var processor2 = CreateArgumentProcessor(); + GlobalFFOptions.Configure(new FFOptions { WorkingDirectory = globalWorkingDir }); var options2 = processor2.GetConfiguredOptions(null); - options2.WorkingDirectory.Should().Be(globalWorkingDir); + Assert.AreEqual(globalWorkingDir, options2.WorkingDirectory); } - - [TestMethod] - public void Concat_Escape() + finally { - var arg = new DemuxConcatArgument(new[] { @"Heaven's River\05 - Investigation.m4b" }); - 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"); + GlobalFFOptions.Configure(new FFOptions()); } } } diff --git a/FFMpegCore.Test/FFMpegCore.Test.csproj b/FFMpegCore.Test/FFMpegCore.Test.csproj index 8413f84..16154a4 100644 --- a/FFMpegCore.Test/FFMpegCore.Test.csproj +++ b/FFMpegCore.Test/FFMpegCore.Test.csproj @@ -5,6 +5,7 @@ false disable default + true @@ -12,21 +13,20 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + - - - + + + + diff --git a/FFMpegCore.Test/FFMpegOptionsTests.cs b/FFMpegCore.Test/FFMpegOptionsTests.cs index 7cab476..4cc9ef7 100644 --- a/FFMpegCore.Test/FFMpegOptionsTests.cs +++ b/FFMpegCore.Test/FFMpegOptionsTests.cs @@ -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] - public class FFMpegOptionsTest + [TestMethod] + public void Options_Initialized() { - [TestMethod] - public void Options_Initialized() - { - Assert.IsNotNull(GlobalFFOptions.Current); - } + Assert.IsNotNull(GlobalFFOptions.Current); + } - [TestMethod] - public void Options_Defaults_Configured() - { - Assert.AreEqual(new FFOptions().BinaryFolder, $""); - } + [TestMethod] + public void Options_Defaults_Configured() + { + Assert.AreEqual("", new FFOptions().BinaryFolder); + } - [TestMethod] - public void Options_Loaded_From_File() - { - Assert.AreEqual( - GlobalFFOptions.Current.BinaryFolder, - JsonConvert.DeserializeObject(File.ReadAllText("ffmpeg.config.json")).BinaryFolder - ); - } + [TestMethod] + public void Options_Loaded_From_File() + { + Assert.AreEqual( + GlobalFFOptions.Current.BinaryFolder, + JsonConvert.DeserializeObject(File.ReadAllText("ffmpeg.config.json")).BinaryFolder + ); + } - [TestMethod] - public void Options_Set_Programmatically() + [TestMethod] + public void ZZZ_Options_Set_Programmatically() + { + try { - var original = GlobalFFOptions.Current; - try - { - GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "Whatever" }); - Assert.AreEqual( - GlobalFFOptions.Current.BinaryFolder, - "Whatever" - ); - } - finally - { - GlobalFFOptions.Configure(original); - } + GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "Whatever" }); + Assert.AreEqual("Whatever", GlobalFFOptions.Current.BinaryFolder); + } + finally + { + GlobalFFOptions.Configure(new FFOptions()); } } } diff --git a/FFMpegCore.Test/FFProbeTests.cs b/FFMpegCore.Test/FFProbeTests.cs index 6ebad0f..a702eed 100644 --- a/FFMpegCore.Test/FFProbeTests.cs +++ b/FFMpegCore.Test/FFProbeTests.cs @@ -1,267 +1,288 @@ using FFMpegCore.Test.Resources; -using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace FFMpegCore.Test +namespace FFMpegCore.Test; + +[TestClass] +public class FFProbeTests { - [TestClass] - public class FFProbeTests + public TestContext TestContext { get; set; } + + [TestMethod] + public async Task Audio_FromStream_Duration() { - [TestMethod] - public async Task Audio_FromStream_Duration() - { - var fileAnalysis = await FFProbe.AnalyseAsync(TestResources.WebmVideo); - await using var inputStream = File.OpenRead(TestResources.WebmVideo); - var streamAnalysis = await FFProbe.AnalyseAsync(inputStream); - Assert.IsTrue(fileAnalysis.Duration == streamAnalysis.Duration); - } + var fileAnalysis = await FFProbe.AnalyseAsync(TestResources.WebmVideo, cancellationToken: TestContext.CancellationToken); + await using var inputStream = File.OpenRead(TestResources.WebmVideo); + var streamAnalysis = await FFProbe.AnalyseAsync(inputStream, cancellationToken: TestContext.CancellationToken); + Assert.IsTrue(fileAnalysis.Duration == streamAnalysis.Duration); + } - [TestMethod] - public void FrameAnalysis_Sync() - { - var frameAnalysis = FFProbe.GetFrames(TestResources.WebmVideo); + [TestMethod] + public void FrameAnalysis_Sync() + { + var frameAnalysis = FFProbe.GetFrames(TestResources.WebmVideo); - Assert.AreEqual(90, frameAnalysis.Frames.Count); - 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.Width == 640)); - Assert.IsTrue(frameAnalysis.Frames.All(f => f.MediaType == "video")); - } + Assert.HasCount(90, frameAnalysis.Frames); + 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.Width == 640)); + Assert.IsTrue(frameAnalysis.Frames.All(f => f.MediaType == "video")); + } - [TestMethod] - public async Task FrameAnalysis_Async() - { - var frameAnalysis = await FFProbe.GetFramesAsync(TestResources.WebmVideo); + [TestMethod] + public async Task FrameAnalysis_Async() + { + var frameAnalysis = await FFProbe.GetFramesAsync(TestResources.WebmVideo, cancellationToken: TestContext.CancellationToken); - Assert.AreEqual(90, frameAnalysis.Frames.Count); - 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.Width == 640)); - Assert.IsTrue(frameAnalysis.Frames.All(f => f.MediaType == "video")); - } + Assert.HasCount(90, frameAnalysis.Frames); + 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.Width == 640)); + Assert.IsTrue(frameAnalysis.Frames.All(f => f.MediaType == "video")); + } - [TestMethod] - public async Task PacketAnalysis_Async() - { - var packetAnalysis = await FFProbe.GetPacketsAsync(TestResources.WebmVideo); - var packets = packetAnalysis.Packets; - Assert.AreEqual(96, packets.Count); - Assert.IsTrue(packets.All(f => f.CodecType == "video")); - Assert.IsTrue(packets[0].Flags.StartsWith("K_")); - Assert.AreEqual(1362, packets.Last().Size); - } + [TestMethod] + public async Task PacketAnalysis_Async() + { + var packetAnalysis = await FFProbe.GetPacketsAsync(TestResources.WebmVideo, cancellationToken: TestContext.CancellationToken); + var packets = packetAnalysis.Packets; + Assert.HasCount(96, packets); + Assert.IsTrue(packets.All(f => f.CodecType == "video")); + Assert.StartsWith("K_", packets[0].Flags); + Assert.AreEqual(1362, packets.Last().Size); + } - [TestMethod] - public void PacketAnalysis_Sync() - { - var packets = FFProbe.GetPackets(TestResources.WebmVideo).Packets; + [TestMethod] + public void PacketAnalysis_Sync() + { + var packets = FFProbe.GetPackets(TestResources.WebmVideo).Packets; - Assert.AreEqual(96, packets.Count); - Assert.IsTrue(packets.All(f => f.CodecType == "video")); - Assert.IsTrue(packets[0].Flags.StartsWith("K_")); - Assert.AreEqual(1362, packets.Last().Size); - } + Assert.HasCount(96, packets); + Assert.IsTrue(packets.All(f => f.CodecType == "video")); + Assert.StartsWith("K_", packets[0].Flags); + Assert.AreEqual(1362, packets.Last().Size); + } - [TestMethod] - public void PacketAnalysisAudioVideo_Sync() - { - var packets = FFProbe.GetPackets(TestResources.Mp4Video).Packets; + [TestMethod] + public void PacketAnalysisAudioVideo_Sync() + { + var packets = FFProbe.GetPackets(TestResources.Mp4Video).Packets; - Assert.AreEqual(216, packets.Count); - var actual = packets.Select(f => f.CodecType).Distinct().ToList(); - var expected = new List { "audio", "video" }; - CollectionAssert.AreEquivalent(expected, actual); - 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(141, packets.Count(t => t.CodecType == "audio")); - } + Assert.HasCount(216, packets); + var actual = packets.Select(f => f.CodecType).Distinct().ToList(); + var expected = new List { "audio", "video" }; + CollectionAssert.AreEquivalent(expected, actual); + 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(141, packets.Count(t => t.CodecType == "audio")); + } - [DataTestMethod] - [DataRow("0:00:03.008000", 0, 0, 0, 3, 8)] - [DataRow("05:12:59.177", 0, 5, 12, 59, 177)] - [DataRow("149:07:50.911750", 6, 5, 7, 50, 911)] - [DataRow("00:00:00.83", 0, 0, 0, 0, 830)] - public void MediaAnalysis_ParseDuration(string duration, int expectedDays, int expectedHours, int expectedMinutes, int expectedSeconds, int expectedMilliseconds) - { - var ffprobeStream = new FFProbeStream { Duration = duration }; + [TestMethod] + [DataRow("0:00:03.008000", 0, 0, 0, 3, 8)] + [DataRow("05:12:59.177", 0, 5, 12, 59, 177)] + [DataRow("149:07:50.911750", 6, 5, 7, 50, 911)] + [DataRow("00:00:00.83", 0, 0, 0, 0, 830)] + public void MediaAnalysis_ParseDuration(string duration, int expectedDays, int expectedHours, int expectedMinutes, int expectedSeconds, + int expectedMilliseconds) + { + 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(expectedHours, parsedDuration.Hours); - Assert.AreEqual(expectedMinutes, parsedDuration.Minutes); - Assert.AreEqual(expectedSeconds, parsedDuration.Seconds); - Assert.AreEqual(expectedMilliseconds, parsedDuration.Milliseconds); - } + Assert.AreEqual(expectedDays, parsedDuration.Days); + Assert.AreEqual(expectedHours, parsedDuration.Hours); + Assert.AreEqual(expectedMinutes, parsedDuration.Minutes); + Assert.AreEqual(expectedSeconds, parsedDuration.Seconds); + Assert.AreEqual(expectedMilliseconds, parsedDuration.Milliseconds); + } - [TestMethod, 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); - } + [TestMethod] + [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"), + cancellationToken: TestContext.CancellationToken); + Assert.IsNotNull(fileAnalysis); + } - [TestMethod] - public void Probe_Success() - { - var info = FFProbe.Analyse(TestResources.Mp4Video); - Assert.AreEqual(3, info.Duration.Seconds); - Assert.AreEqual(0, info.Chapters.Count); + [TestMethod] + public void Probe_Success() + { + var info = FFProbe.Analyse(TestResources.Mp4Video); + Assert.AreEqual(3, info.Duration.Seconds); + Assert.IsEmpty(info.Chapters); - Assert.AreEqual("5.1", info.PrimaryAudioStream!.ChannelLayout); - Assert.AreEqual(6, info.PrimaryAudioStream.Channels); - Assert.AreEqual("AAC (Advanced Audio Coding)", info.PrimaryAudioStream.CodecLongName); - Assert.AreEqual("aac", info.PrimaryAudioStream.CodecName); - Assert.AreEqual("LC", info.PrimaryAudioStream.Profile); - Assert.AreEqual(377351, info.PrimaryAudioStream.BitRate); - Assert.AreEqual(48000, info.PrimaryAudioStream.SampleRateHz); - Assert.AreEqual("mp4a", info.PrimaryAudioStream.CodecTagString); - Assert.AreEqual("0x6134706d", info.PrimaryAudioStream.CodecTag); + Assert.AreEqual("5.1", info.PrimaryAudioStream!.ChannelLayout); + Assert.AreEqual(6, info.PrimaryAudioStream.Channels); + Assert.AreEqual("AAC (Advanced Audio Coding)", info.PrimaryAudioStream.CodecLongName); + Assert.AreEqual("aac", info.PrimaryAudioStream.CodecName); + Assert.AreEqual("LC", info.PrimaryAudioStream.Profile); + Assert.AreEqual(377351, info.PrimaryAudioStream.BitRate); + Assert.AreEqual(48000, info.PrimaryAudioStream.SampleRateHz); + Assert.AreEqual("mp4a", info.PrimaryAudioStream.CodecTagString); + Assert.AreEqual("0x6134706d", info.PrimaryAudioStream.CodecTag); - Assert.AreEqual(1471810, info.PrimaryVideoStream!.BitRate); - Assert.AreEqual(16, info.PrimaryVideoStream.DisplayAspectRatio.Width); - Assert.AreEqual(9, info.PrimaryVideoStream.DisplayAspectRatio.Height); - Assert.AreEqual(1, info.PrimaryVideoStream.SampleAspectRatio.Width); - Assert.AreEqual(1, info.PrimaryVideoStream.SampleAspectRatio.Height); - Assert.AreEqual("yuv420p", info.PrimaryVideoStream.PixelFormat); - Assert.AreEqual(31, info.PrimaryVideoStream.Level); - Assert.AreEqual(1280, info.PrimaryVideoStream.Width); - Assert.AreEqual(720, info.PrimaryVideoStream.Height); - Assert.AreEqual(25, info.PrimaryVideoStream.AvgFrameRate); - Assert.AreEqual(25, info.PrimaryVideoStream.FrameRate); - Assert.AreEqual("H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10", info.PrimaryVideoStream.CodecLongName); - Assert.AreEqual("h264", info.PrimaryVideoStream.CodecName); - Assert.AreEqual(8, info.PrimaryVideoStream.BitsPerRawSample); - Assert.AreEqual("Main", info.PrimaryVideoStream.Profile); - Assert.AreEqual("avc1", info.PrimaryVideoStream.CodecTagString); - Assert.AreEqual("0x31637661", info.PrimaryVideoStream.CodecTag); - } + Assert.AreEqual(1471810, info.PrimaryVideoStream!.BitRate); + Assert.AreEqual(16, info.PrimaryVideoStream.DisplayAspectRatio.Width); + Assert.AreEqual(9, info.PrimaryVideoStream.DisplayAspectRatio.Height); + Assert.AreEqual(1, info.PrimaryVideoStream.SampleAspectRatio.Width); + Assert.AreEqual(1, info.PrimaryVideoStream.SampleAspectRatio.Height); + Assert.AreEqual("yuv420p", info.PrimaryVideoStream.PixelFormat); + Assert.AreEqual(31, info.PrimaryVideoStream.Level); + Assert.AreEqual(1280, info.PrimaryVideoStream.Width); + Assert.AreEqual(720, info.PrimaryVideoStream.Height); + Assert.AreEqual(25, info.PrimaryVideoStream.AvgFrameRate); + Assert.AreEqual(25, info.PrimaryVideoStream.FrameRate); + Assert.AreEqual("H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10", info.PrimaryVideoStream.CodecLongName); + Assert.AreEqual("h264", info.PrimaryVideoStream.CodecName); + Assert.AreEqual(8, info.PrimaryVideoStream.BitsPerRawSample); + Assert.AreEqual("Main", info.PrimaryVideoStream.Profile); + Assert.AreEqual("avc1", info.PrimaryVideoStream.CodecTagString); + Assert.AreEqual("0x31637661", info.PrimaryVideoStream.CodecTag); + } - [TestMethod] - public void Probe_Rotation() - { - var info = FFProbe.Analyse(TestResources.Mp4Video); - Assert.AreEqual(0, info.PrimaryVideoStream.Rotation); + [TestMethod] + public void Probe_Rotation() + { + var info = FFProbe.Analyse(TestResources.Mp4Video); + Assert.IsNotNull(info.PrimaryVideoStream); + Assert.AreEqual(0, info.PrimaryVideoStream.Rotation); - info = FFProbe.Analyse(TestResources.Mp4VideoRotation); - Assert.AreEqual(90, info.PrimaryVideoStream.Rotation); - } + info = FFProbe.Analyse(TestResources.Mp4VideoRotation); + Assert.IsNotNull(info.PrimaryVideoStream); + Assert.AreEqual(90, info.PrimaryVideoStream.Rotation); + } - [TestMethod] - public void Probe_Rotation_Negative_Value() - { - var info = FFProbe.Analyse(TestResources.Mp4VideoRotationNegative); - Assert.AreEqual(-90, info.PrimaryVideoStream.Rotation); - } + [TestMethod] + public void Probe_Rotation_Negative_Value() + { + var info = FFProbe.Analyse(TestResources.Mp4VideoRotationNegative); + Assert.IsNotNull(info.PrimaryVideoStream); + Assert.AreEqual(-90, info.PrimaryVideoStream.Rotation); + } - [TestMethod, Timeout(10000)] - public async Task Probe_Async_Success() - { - var info = await FFProbe.AnalyseAsync(TestResources.Mp4Video); - Assert.AreEqual(3, info.Duration.Seconds); - Assert.AreEqual(8, info.PrimaryVideoStream.BitDepth); - // This video's audio stream is AAC, which is lossy, so bit depth is meaningless. - Assert.IsNull(info.PrimaryAudioStream.BitDepth); - } + [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] + public async Task Probe_Async_Success() + { + var info = await FFProbe.AnalyseAsync(TestResources.Mp4Video, cancellationToken: TestContext.CancellationToken); + Assert.AreEqual(3, info.Duration.Seconds); + Assert.IsNotNull(info.PrimaryVideoStream); + 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)] - public void Probe_Success_FromStream() - { - using var stream = File.OpenRead(TestResources.WebmVideo); - var info = FFProbe.Analyse(stream); - Assert.AreEqual(3, info.Duration.Seconds); - // This video has no audio stream. - Assert.IsNull(info.PrimaryAudioStream); - } + [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] + public void Probe_Success_FromStream() + { + using var stream = File.OpenRead(TestResources.WebmVideo); + var info = FFProbe.Analyse(stream); + Assert.AreEqual(3, info.Duration.Seconds); + // This video has no audio stream. + Assert.IsNull(info.PrimaryAudioStream); + } - [TestMethod, Timeout(10000)] - public async Task Probe_Success_FromStream_Async() - { - await using var stream = File.OpenRead(TestResources.WebmVideo); - var info = await FFProbe.AnalyseAsync(stream); - Assert.AreEqual(3, info.Duration.Seconds); - } + [TestMethod] + [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, cancellationToken: TestContext.CancellationToken); + Assert.AreEqual(3, info.Duration.Seconds); + } - [TestMethod, Timeout(10000)] - public void Probe_HDR() - { - var info = FFProbe.Analyse(TestResources.HdrVideo); + [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] + public void Probe_HDR() + { + var info = FFProbe.Analyse(TestResources.HdrVideo); - Assert.IsNotNull(info.PrimaryVideoStream); - Assert.AreEqual("tv", info.PrimaryVideoStream.ColorRange); - Assert.AreEqual("bt2020nc", info.PrimaryVideoStream.ColorSpace); - Assert.AreEqual("arib-std-b67", info.PrimaryVideoStream.ColorTransfer); - Assert.AreEqual("bt2020", info.PrimaryVideoStream.ColorPrimaries); - } + Assert.IsNotNull(info.PrimaryVideoStream); + Assert.AreEqual("tv", info.PrimaryVideoStream.ColorRange); + Assert.AreEqual("bt2020nc", info.PrimaryVideoStream.ColorSpace); + Assert.AreEqual("arib-std-b67", info.PrimaryVideoStream.ColorTransfer); + Assert.AreEqual("bt2020", info.PrimaryVideoStream.ColorPrimaries); + } - [TestMethod, Timeout(10000)] - public async Task Probe_Success_Subtitle_Async() - { - var info = await FFProbe.AnalyseAsync(TestResources.SrtSubtitle); - Assert.IsNotNull(info.PrimarySubtitleStream); - Assert.AreEqual(1, info.SubtitleStreams.Count); - Assert.AreEqual(0, info.AudioStreams.Count); - Assert.AreEqual(0, info.VideoStreams.Count); - // BitDepth is meaningless for subtitles - Assert.IsNull(info.SubtitleStreams[0].BitDepth); - } + [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] + public async Task Probe_Success_Subtitle_Async() + { + var info = await FFProbe.AnalyseAsync(TestResources.SrtSubtitle, cancellationToken: TestContext.CancellationToken); + Assert.IsNotNull(info.PrimarySubtitleStream); + Assert.HasCount(1, info.SubtitleStreams); + Assert.IsEmpty(info.AudioStreams); + Assert.IsEmpty(info.VideoStreams); + // BitDepth is meaningless for subtitles + Assert.IsNull(info.SubtitleStreams[0].BitDepth); + } - [TestMethod, Timeout(10000)] - public async Task Probe_Success_Disposition_Async() - { - var info = await FFProbe.AnalyseAsync(TestResources.Mp4Video); - Assert.IsNotNull(info.PrimaryAudioStream); - Assert.IsNotNull(info.PrimaryAudioStream.Disposition); - Assert.AreEqual(true, info.PrimaryAudioStream.Disposition["default"]); - Assert.AreEqual(false, info.PrimaryAudioStream.Disposition["forced"]); - } + [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] + public async Task Probe_Success_Disposition_Async() + { + var info = await FFProbe.AnalyseAsync(TestResources.Mp4Video, cancellationToken: TestContext.CancellationToken); + Assert.IsNotNull(info.PrimaryAudioStream); + Assert.IsNotNull(info.PrimaryAudioStream.Disposition); + Assert.IsTrue(info.PrimaryAudioStream.Disposition["default"]); + Assert.IsFalse(info.PrimaryAudioStream.Disposition["forced"]); + } - [TestMethod, Timeout(10000)] - public async Task Probe_Success_Mp3AudioBitDepthNull_Async() - { - var info = await FFProbe.AnalyseAsync(TestResources.Mp3Audio); - Assert.IsNotNull(info.PrimaryAudioStream); - // mp3 is lossy, so bit depth is meaningless. - Assert.IsNull(info.PrimaryAudioStream.BitDepth); - } + [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] + public async Task Probe_Success_Mp3AudioBitDepthNull_Async() + { + var info = await FFProbe.AnalyseAsync(TestResources.Mp3Audio, cancellationToken: TestContext.CancellationToken); + Assert.IsNotNull(info.PrimaryAudioStream); + // mp3 is lossy, so bit depth is meaningless. + Assert.IsNull(info.PrimaryAudioStream.BitDepth); + } - [TestMethod, Timeout(10000)] - public async Task Probe_Success_VocAudioBitDepth_Async() - { - var info = await FFProbe.AnalyseAsync(TestResources.AiffAudio); - Assert.IsNotNull(info.PrimaryAudioStream); - Assert.AreEqual(16, info.PrimaryAudioStream.BitDepth); - } + [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] + public async Task Probe_Success_VocAudioBitDepth_Async() + { + var info = await FFProbe.AnalyseAsync(TestResources.AiffAudio, cancellationToken: TestContext.CancellationToken); + Assert.IsNotNull(info.PrimaryAudioStream); + Assert.AreEqual(16, info.PrimaryAudioStream.BitDepth); + } - [TestMethod, Timeout(10000)] - public async Task Probe_Success_MkvVideoBitDepth_Async() - { - var info = await FFProbe.AnalyseAsync(TestResources.MkvVideo); - Assert.IsNotNull(info.PrimaryAudioStream); - Assert.AreEqual(8, info.PrimaryVideoStream.BitDepth); - Assert.IsNull(info.PrimaryAudioStream.BitDepth); - } + [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] + public async Task Probe_Success_MkvVideoBitDepth_Async() + { + var info = await FFProbe.AnalyseAsync(TestResources.MkvVideo, cancellationToken: TestContext.CancellationToken); + Assert.IsNotNull(info.PrimaryVideoStream); + Assert.AreEqual(8, info.PrimaryVideoStream.BitDepth); - [TestMethod, Timeout(10000)] - public async Task Probe_Success_24BitWavBitDepth_Async() - { - var info = await FFProbe.AnalyseAsync(TestResources.Wav24Bit); - Assert.IsNotNull(info.PrimaryAudioStream); - Assert.AreEqual(24, info.PrimaryAudioStream.BitDepth); - } + Assert.IsNotNull(info.PrimaryAudioStream); + Assert.IsNull(info.PrimaryAudioStream.BitDepth); + } - [TestMethod, Timeout(10000)] - public async Task Probe_Success_32BitWavBitDepth_Async() - { - var info = await FFProbe.AnalyseAsync(TestResources.Wav32Bit); - Assert.IsNotNull(info.PrimaryAudioStream); - Assert.AreEqual(32, info.PrimaryAudioStream.BitDepth); - } + [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] + public async Task Probe_Success_24BitWavBitDepth_Async() + { + var info = await FFProbe.AnalyseAsync(TestResources.Wav24Bit, cancellationToken: TestContext.CancellationToken); + Assert.IsNotNull(info.PrimaryAudioStream); + Assert.AreEqual(24, 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); - } + [TestMethod] + [Timeout(10000, CooperativeCancellation = true)] + public async Task Probe_Success_32BitWavBitDepth_Async() + { + 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); } } diff --git a/FFMpegCore.Test/MetaDataBuilderTests.cs b/FFMpegCore.Test/MetaDataBuilderTests.cs index 3a2b81a..a9a693a 100644 --- a/FFMpegCore.Test/MetaDataBuilderTests.cs +++ b/FFMpegCore.Test/MetaDataBuilderTests.cs @@ -1,70 +1,66 @@ using System.Text.RegularExpressions; using FFMpegCore.Builders.MetaData; -using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace FFMpegCore.Test +namespace FFMpegCore.Test; + +[TestClass] +public class MetaDataBuilderTests { - [TestClass] - public class MetaDataBuilderTests + [TestMethod] + public void TestMetaDataBuilderIntegrity() { - [TestMethod] - public void TestMetaDataBuilderIntegrity() + var source = new { - 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", - Artist = "Pachelbel", - 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" }, - } - }; + 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() - .WithTitle(source.Title) - .WithArtists(source.Artist) - .WithComposers(source.Composer) - .WithAlbumArtists(source.Artist) - .WithGenres(source.Genres) - .WithCopyright(source.Copyright) - .AddChapters(source.Tracks, x => (x.Duration, x.Title)); + var builder = new MetaDataBuilder() + .WithTitle(source.Title) + .WithArtists(source.Artist) + .WithComposers(source.Composer) + .WithAlbumArtists(source.Artist) + .WithGenres(source.Genres) + .WithCopyright(source.Copyright) + .AddChapters(source.Tracks, x => (x.Duration, x.Title)); - var metadata = builder.Build(); - var serialized = MetaDataSerializer.Instance.Serialize(metadata); + var metadata = builder.Build(); + var serialized = MetaDataSerializer.Instance.Serialize(metadata); - Assert.IsTrue(serialized.StartsWith(";FFMETADATA1", StringComparison.OrdinalIgnoreCase)); - Assert.IsTrue(serialized.Contains("genre=Synthwave; Classics", StringComparison.OrdinalIgnoreCase)); - Assert.IsTrue(serialized.Contains("title=Chapter 01", StringComparison.OrdinalIgnoreCase)); - Assert.IsTrue(serialized.Contains("album_artist=Pachelbel", StringComparison.OrdinalIgnoreCase)); - } + Assert.IsTrue(serialized.StartsWith(";FFMETADATA1", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(serialized.Contains("genre=Synthwave; Classics", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(serialized.Contains("title=Chapter 01", StringComparison.OrdinalIgnoreCase)); + Assert.IsTrue(serialized.Contains("album_artist=Pachelbel", StringComparison.OrdinalIgnoreCase)); + } - [TestMethod] - public void TestMapMetadata() - { - //-i "whaterver0" // index: 0 - //-f concat -safe 0 - //-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 - //-map_metadata 2 + [TestMethod] + public void TestMapMetadata() + { + //-i "whaterver0" // index: 0 + //-f concat -safe 0 + //-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 + //-map_metadata 2 - var text0 = FFMpegArguments.FromFileInput("whaterver0") - .AddMetaData("WhatEver3") - .Text; + var text0 = FFMpegArguments.FromFileInput("whaterver0") + .AddMetaData("WhatEver3") + .Text; - var text1 = FFMpegArguments.FromFileInput("whaterver0") - .AddDemuxConcatInput(new[] { "whaterver", "whaterver1" }) - .AddMetaData("WhatEver3") - .Text; + var text1 = FFMpegArguments.FromFileInput("whaterver0") + .AddDemuxConcatInput(new[] { "whaterver", "whaterver1" }) + .AddMetaData("WhatEver3") + .Text; - 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(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."); } } diff --git a/FFMpegCore.Test/PixelFormatTests.cs b/FFMpegCore.Test/PixelFormatTests.cs index ed69f11..216ce2a 100644 --- a/FFMpegCore.Test/PixelFormatTests.cs +++ b/FFMpegCore.Test/PixelFormatTests.cs @@ -1,41 +1,39 @@ using FFMpegCore.Exceptions; -using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace FFMpegCore.Test +namespace FFMpegCore.Test; + +[TestClass] +public class PixelFormatTests { - [TestClass] - public class PixelFormatTests + [TestMethod] + public void PixelFormats_Enumerate() { - [TestMethod] - public void PixelFormats_Enumerate() - { - var formats = FFMpeg.GetPixelFormats(); - Assert.IsTrue(formats.Count > 0); - } + var formats = FFMpeg.GetPixelFormats(); + Assert.IsNotEmpty(formats); + } - [TestMethod] - public void PixelFormats_TryGetExisting() - { - Assert.IsTrue(FFMpeg.TryGetPixelFormat("yuv420p", out _)); - } + [TestMethod] + public void PixelFormats_TryGetExisting() + { + Assert.IsTrue(FFMpeg.TryGetPixelFormat("yuv420p", out _)); + } - [TestMethod] - public void PixelFormats_TryGetNotExisting() - { - Assert.IsFalse(FFMpeg.TryGetPixelFormat("yuv420pppUnknown", out _)); - } + [TestMethod] + public void PixelFormats_TryGetNotExisting() + { + Assert.IsFalse(FFMpeg.TryGetPixelFormat("yuv420pppUnknown", out _)); + } - [TestMethod] - public void PixelFormats_GetExisting() - { - var fmt = FFMpeg.GetPixelFormat("yuv420p"); - Assert.IsTrue(fmt.Components == 3 && fmt.BitsPerPixel == 12); - } + [TestMethod] + public void PixelFormats_GetExisting() + { + var fmt = FFMpeg.GetPixelFormat("yuv420p"); + Assert.IsTrue(fmt.Components == 3 && fmt.BitsPerPixel == 12); + } - [TestMethod] - public void PixelFormats_GetNotExisting() - { - Assert.ThrowsException(() => FFMpeg.GetPixelFormat("yuv420pppUnknown")); - } + [TestMethod] + public void PixelFormats_GetNotExisting() + { + Assert.ThrowsExactly(() => FFMpeg.GetPixelFormat("yuv420pppUnknown")); } } diff --git a/FFMpegCore.Test/Resources/TestResources.cs b/FFMpegCore.Test/Resources/TestResources.cs index 97e7892..8e2e815 100644 --- a/FFMpegCore.Test/Resources/TestResources.cs +++ b/FFMpegCore.Test/Resources/TestResources.cs @@ -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 Mp4VideoRotationNegative = "./Resources/input_3sec_rotation_negative_90deg.mp4"; - public static readonly string WebmVideo = "./Resources/input_3sec.webm"; - public static readonly string HdrVideo = "./Resources/input_hdr.mov"; - public static readonly string Mp4WithoutVideo = "./Resources/input_audio_only_10sec.mp4"; - public static readonly string Mp4WithoutAudio = "./Resources/input_video_only_3sec.mp4"; - public static readonly string RawAudio = "./Resources/audio.raw"; - public static readonly string Mp3Audio = "./Resources/audio.mp3"; - public static readonly string PngImage = "./Resources/cover.png"; - public static readonly string ImageCollection = "./Resources/images"; - public static readonly string SrtSubtitle = "./Resources/sample.srt"; - public static readonly string AiffAudio = "./Resources/sample3aiff.aiff"; - public static readonly string MkvVideo = "./Resources/sampleMKV.mkv"; - public static readonly string Wav24Bit = "./Resources/24_bit_fixed.WAV"; - public static readonly string Wav32Bit = "./Resources/32_bit_float.WAV"; - } + public static readonly string Mp4Video = "./Resources/input_3sec.mp4"; + public static readonly string Mp4VideoRotation = "./Resources/input_3sec_rotation_90deg.mp4"; + public static readonly string Mp4VideoRotationNegative = "./Resources/input_3sec_rotation_negative_90deg.mp4"; + public static readonly string WebmVideo = "./Resources/input_3sec.webm"; + public static readonly string HdrVideo = "./Resources/input_hdr.mov"; + public static readonly string Mp4WithoutVideo = "./Resources/input_audio_only_10sec.mp4"; + public static readonly string Mp4WithoutAudio = "./Resources/input_video_only_3sec.mp4"; + public static readonly string RawAudio = "./Resources/audio.raw"; + public static readonly string Mp3Audio = "./Resources/audio.mp3"; + public static readonly string PngImage = "./Resources/cover.png"; + public static readonly string ImageCollection = "./Resources/images"; + public static readonly string SrtSubtitle = "./Resources/sample.srt"; + public static readonly string AiffAudio = "./Resources/sample3aiff.aiff"; + public static readonly string MkvVideo = "./Resources/sampleMKV.mkv"; + public static readonly string Wav24Bit = "./Resources/24_bit_fixed.WAV"; + public static readonly string Wav32Bit = "./Resources/32_bit_float.WAV"; } diff --git a/FFMpegCore.Test/TemporaryFile.cs b/FFMpegCore.Test/TemporaryFile.cs index dc30ca4..7bc2b33 100644 --- a/FFMpegCore.Test/TemporaryFile.cs +++ b/FFMpegCore.Test/TemporaryFile.cs @@ -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}"); - } - - public static implicit operator string(TemporaryFile temporaryFile) => temporaryFile._path; - public void Dispose() - { - if (File.Exists(_path)) - { - File.Delete(_path); - } + File.Delete(_path); } } + + public static implicit operator string(TemporaryFile temporaryFile) + { + return temporaryFile._path; + } } diff --git a/FFMpegCore.Test/Utilities/BitmapSources.cs b/FFMpegCore.Test/Utilities/BitmapSources.cs index f3b657a..02b8af3 100644 --- a/FFMpegCore.Test/Utilities/BitmapSources.cs +++ b/FFMpegCore.Test/Utilities/BitmapSources.cs @@ -2,254 +2,251 @@ using System.Drawing.Imaging; using System.Numerics; using System.Runtime.Versioning; +using FFMpegCore.Extensions.System.Drawing.Common; using FFMpegCore.Pipes; using SkiaSharp; -namespace FFMpegCore.Test.Utilities +namespace FFMpegCore.Test.Utilities; + +internal static class BitmapSource { - internal static class BitmapSource + [SupportedOSPlatform("windows")] + public static IEnumerable CreateBitmaps(int count, PixelFormat fmt, int w, int h) { - [SupportedOSPlatform("windows")] - public static IEnumerable CreateBitmaps(int count, PixelFormat 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; } } - - public static IEnumerable CreateBitmaps(int count, SKColorType 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)) - { - 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) - { - var bitmap = new Bitmap(w, h, fmt); - - 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); - } - - return new Extensions.System.Drawing.Common.BitmapVideoFrameWrapper(bitmap); - } - - 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); - - bitmap.Pixels = GenerateVideoFramePixels(index, w, h, scaleNoise, offset) - .Select(args => new SKColor(args.red, args.blue, args.green)) - .ToArray(); - - 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) - { - offset = offset * index; - - for (var y = 0; y < h; y++) - { - for (var x = 0; x < w; x++) - { - var xf = x / (float)w; - var yf = y / (float)h; - var nx = x * scaleNoise + offset; - var ny = y * scaleNoise + offset; - - 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 - // - // Based on the original implementation by Ken Perlin - // http://mrl.nyu.edu/~perlin/noise/ - // - private static class Perlin - { - #region Noise functions - - public static float Noise(float x) - { - var X = (int)MathF.Floor(x) & 0xff; - 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; - var Y = (int)MathF.Floor(y) & 0xff; - x -= MathF.Floor(x); - y -= MathF.Floor(y); - var u = Fade(x); - var v = Fade(y); - var A = (perm[X] + Y) & 0xff; - 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(float x, float y, float z) - { - var X = (int)MathF.Floor(x) & 0xff; - var Y = (int)MathF.Floor(y) & 0xff; - var Z = (int)MathF.Floor(z) & 0xff; - x -= MathF.Floor(x); - y -= MathF.Floor(y); - z -= MathF.Floor(z); - var u = Fade(x); - var v = Fade(y); - var w = Fade(z); - var A = (perm[X] + Y) & 0xff; - var B = (perm[X + 1] + Y) & 0xff; - var AA = (perm[A] + Z) & 0xff; - var BA = (perm[B] + Z) & 0xff; - var AB = (perm[A + 1] + Z) & 0xff; - var BB = (perm[B + 1] + Z) & 0xff; - 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], 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); - } - - #endregion - - #region fBm functions - - public static float Fbm(float x, int octave) - { - var f = 0.0f; - var w = 0.5f; - for (var i = 0; i < octave; i++) - { - f += w * Noise(x); - x *= 2.0f; - w *= 0.5f; - } - - return f; - } - - public static float Fbm(Vector2 coord, int octave) - { - var f = 0.0f; - var w = 0.5f; - for (var i = 0; i < octave; i++) - { - f += w * Noise(coord); - coord *= 2.0f; - w *= 0.5f; - } - - return f; - } - - 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; - var w = 0.5f; - for (var i = 0; i < octave; i++) - { - f += w * Noise(coord); - coord *= 2.0f; - w *= 0.5f; - } - - return f; - } - - public static float Fbm(float x, float y, float z, int octave) - { - return Fbm(new Vector3(x, y, z), octave); - } - - #endregion - - #region Private functions - - 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 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, float z) - { - var h = hash & 15; - 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, - 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, - 77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244, - 102,143,54, 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,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,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,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, 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,78,66,215,61,156,180, - 151 - }; - - #endregion - } + } + + public static IEnumerable CreateBitmaps(int count, SKColorType 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)) + { + yield return frame; + } + } + } + + [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); + + 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); + } + + return new BitmapVideoFrameWrapper(bitmap); + } + + 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); + + bitmap.Pixels = GenerateVideoFramePixels(index, w, h, scaleNoise, offset) + .Select(args => new SKColor(args.red, args.blue, args.green)) + .ToArray(); + + 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) + { + offset = offset * index; + + for (var y = 0; y < h; y++) + { + for (var x = 0; x < w; x++) + { + var xf = x / (float)w; + var yf = y / (float)h; + var nx = x * scaleNoise + offset; + var ny = y * scaleNoise + offset; + + 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 + // + // Based on the original implementation by Ken Perlin + // http://mrl.nyu.edu/~perlin/noise/ + // + private static class Perlin + { + #region Noise functions + + public static float Noise(float x) + { + var X = (int)MathF.Floor(x) & 0xff; + 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; + var Y = (int)MathF.Floor(y) & 0xff; + x -= MathF.Floor(x); + y -= MathF.Floor(y); + var u = Fade(x); + var v = Fade(y); + var A = (perm[X] + Y) & 0xff; + 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(float x, float y, float z) + { + var X = (int)MathF.Floor(x) & 0xff; + var Y = (int)MathF.Floor(y) & 0xff; + var Z = (int)MathF.Floor(z) & 0xff; + x -= MathF.Floor(x); + y -= MathF.Floor(y); + z -= MathF.Floor(z); + var u = Fade(x); + var v = Fade(y); + var w = Fade(z); + var A = (perm[X] + Y) & 0xff; + var B = (perm[X + 1] + Y) & 0xff; + var AA = (perm[A] + Z) & 0xff; + var BA = (perm[B] + Z) & 0xff; + var AB = (perm[A + 1] + Z) & 0xff; + var BB = (perm[B + 1] + Z) & 0xff; + 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], 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); + } + + #endregion + + #region fBm functions + + public static float Fbm(float x, int octave) + { + var f = 0.0f; + var w = 0.5f; + for (var i = 0; i < octave; i++) + { + f += w * Noise(x); + x *= 2.0f; + w *= 0.5f; + } + + return f; + } + + public static float Fbm(Vector2 coord, int octave) + { + var f = 0.0f; + var w = 0.5f; + for (var i = 0; i < octave; i++) + { + f += w * Noise(coord); + coord *= 2.0f; + w *= 0.5f; + } + + return f; + } + + 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; + var w = 0.5f; + for (var i = 0; i < octave; i++) + { + f += w * Noise(coord); + coord *= 2.0f; + w *= 0.5f; + } + + return f; + } + + public static float Fbm(float x, float y, float z, int octave) + { + return Fbm(new Vector3(x, y, z), octave); + } + + #endregion + + #region Private functions + + 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 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, float z) + { + var h = hash & 15; + 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, 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, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, 55, 46, 245, 40, 244, 102, 143, 54, + 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, + 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, + 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, + 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, + 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, + 78, 66, 215, 61, 156, 180, 151 + }; + + #endregion } } diff --git a/FFMpegCore.Test/Utilities/OsSpecificTestMethod.cs b/FFMpegCore.Test/Utilities/OsSpecificTestMethod.cs new file mode 100644 index 0000000..df7ebd5 --- /dev/null +++ b/FFMpegCore.Test/Utilities/OsSpecificTestMethod.cs @@ -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 _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 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) } + ]; + } + } +} diff --git a/FFMpegCore.Test/Utilities/WindowsOnlyDataTestMethod.cs b/FFMpegCore.Test/Utilities/WindowsOnlyDataTestMethod.cs deleted file mode 100644 index 84a779a..0000000 --- a/FFMpegCore.Test/Utilities/WindowsOnlyDataTestMethod.cs +++ /dev/null @@ -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); - } -} diff --git a/FFMpegCore.Test/Utilities/WindowsOnlyTestMethod.cs b/FFMpegCore.Test/Utilities/WindowsOnlyTestMethod.cs deleted file mode 100644 index 7e817bf..0000000 --- a/FFMpegCore.Test/Utilities/WindowsOnlyTestMethod.cs +++ /dev/null @@ -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); - } -} diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 8da9c19..7946552 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -5,910 +5,1073 @@ using System.Text; using FFMpegCore.Arguments; using FFMpegCore.Enums; using FFMpegCore.Exceptions; +using FFMpegCore.Extensions.System.Drawing.Common; using FFMpegCore.Pipes; using FFMpegCore.Test.Resources; using FFMpegCore.Test.Utilities; -using Microsoft.VisualStudio.TestTools.UnitTesting; +using SkiaSharp; +using PixelFormat = System.Drawing.Imaging.PixelFormat; -namespace FFMpegCore.Test +namespace FFMpegCore.Test; + +[TestClass] +public class VideoTest { - [TestClass] - public class VideoTest + private const int BaseTimeoutMilliseconds = 15_000; + + public TestContext TestContext { get; set; } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_ToOGV() { - private const int BaseTimeoutMilliseconds = 15_000; + using var outputFile = new TemporaryFile($"out{VideoType.Ogv.Extension}"); - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_ToOGV() + var success = FFMpegArguments + .FromFileInput(TestResources.WebmVideo) + .OutputToFile(outputFile, false) + .ProcessSynchronously(); + Assert.IsTrue(success); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_ToMP4() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var success = FFMpegArguments + .FromFileInput(TestResources.WebmVideo) + .OutputToFile(outputFile, false) + .ProcessSynchronously(); + Assert.IsTrue(success); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_ToMP4_YUV444p() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var success = FFMpegArguments + .FromFileInput(TestResources.WebmVideo) + .OutputToFile(outputFile, false, opt => opt + .WithVideoCodec(VideoCodec.LibX264) + .ForcePixelFormat("yuv444p")) + .ProcessSynchronously(); + Assert.IsTrue(success); + var analysis = FFProbe.Analyse(outputFile); + Assert.AreEqual("yuv444p", analysis.VideoStreams.First().PixelFormat); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_ToMP4_Args() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var success = FFMpegArguments + .FromFileInput(TestResources.WebmVideo) + .OutputToFile(outputFile, false, opt => opt + .WithVideoCodec(VideoCodec.LibX264)) + .ProcessSynchronously(); + Assert.IsTrue(success); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_ToH265_MKV_Args() + { + using var outputFile = new TemporaryFile("out.mkv"); + + var success = FFMpegArguments + .FromFileInput(TestResources.WebmVideo) + .OutputToFile(outputFile, false, opt => opt + .WithVideoCodec(VideoCodec.LibX265)) + .ProcessSynchronously(); + Assert.IsTrue(success); + } + + [SupportedOSPlatform("windows")] + [OsSpecificTestMethod(OsPlatforms.Windows)] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + [DataRow(PixelFormat.Format24bppRgb)] + [DataRow(PixelFormat.Format32bppArgb)] + public void Video_ToMP4_Args_Pipe_WindowsOnly(PixelFormat pixelFormat) + { + Video_ToMP4_Args_Pipe_Internal(pixelFormat); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + [DataRow(SKColorType.Rgb565)] + [DataRow(SKColorType.Bgra8888)] + public void Video_ToMP4_Args_Pipe(SKColorType pixelFormat) + { + Video_ToMP4_Args_Pipe_Internal(pixelFormat); + } + + private static void Video_ToMP4_Args_Pipe_Internal(dynamic pixelFormat) + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var videoFramesSource = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, pixelFormat, 256, 256)); + var success = FFMpegArguments + .FromPipeInput(videoFramesSource) + .OutputToFile(outputFile, false, opt => opt + .WithVideoCodec(VideoCodec.LibX264)) + .ProcessSynchronously(); + Assert.IsTrue(success); + } + + [SupportedOSPlatform("windows")] + [OsSpecificTestMethod(OsPlatforms.Windows)] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_ToMP4_Args_Pipe_DifferentImageSizes_WindowsOnly() + { + Video_ToMP4_Args_Pipe_DifferentImageSizes_Internal(PixelFormat.Format24bppRgb); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_ToMP4_Args_Pipe_DifferentImageSizes() + { + Video_ToMP4_Args_Pipe_DifferentImageSizes_Internal(SKColorType.Rgb565); + } + + private static void Video_ToMP4_Args_Pipe_DifferentImageSizes_Internal(dynamic pixelFormat) + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var frames = new List { - using var outputFile = new TemporaryFile($"out{VideoType.Ogv.Extension}"); + BitmapSource.CreateVideoFrame(0, pixelFormat, 255, 255, 1, 0), BitmapSource.CreateVideoFrame(0, pixelFormat, 256, 256, 1, 0) + }; - var success = FFMpegArguments - .FromFileInput(TestResources.WebmVideo) - .OutputToFile(outputFile, false) - .ProcessSynchronously(); - Assert.IsTrue(success); - } + var videoFramesSource = new RawVideoPipeSource(frames); + Assert.ThrowsExactly(() => FFMpegArguments + .FromPipeInput(videoFramesSource) + .OutputToFile(outputFile, false, opt => opt + .WithVideoCodec(VideoCodec.LibX264)) + .ProcessSynchronously()); + } - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_ToMP4() + [SupportedOSPlatform("windows")] + [OsSpecificTestMethod(OsPlatforms.Windows)] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public async Task Video_ToMP4_Args_Pipe_DifferentImageSizes_WindowsOnly_Async() + { + await Video_ToMP4_Args_Pipe_DifferentImageSizes_Internal_Async(PixelFormat.Format24bppRgb); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public async Task Video_ToMP4_Args_Pipe_DifferentImageSizes_Async() + { + await Video_ToMP4_Args_Pipe_DifferentImageSizes_Internal_Async(SKColorType.Rgb565); + } + + private static async Task Video_ToMP4_Args_Pipe_DifferentImageSizes_Internal_Async(dynamic pixelFormat) + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var frames = new List { - using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + BitmapSource.CreateVideoFrame(0, pixelFormat, 255, 255, 1, 0), BitmapSource.CreateVideoFrame(0, pixelFormat, 256, 256, 1, 0) + }; - var success = FFMpegArguments - .FromFileInput(TestResources.WebmVideo) - .OutputToFile(outputFile, false) - .ProcessSynchronously(); - Assert.IsTrue(success); - } + var videoFramesSource = new RawVideoPipeSource(frames); + await Assert.ThrowsExactlyAsync(() => FFMpegArguments + .FromPipeInput(videoFramesSource) + .OutputToFile(outputFile, false, opt => opt + .WithVideoCodec(VideoCodec.LibX264)) + .ProcessAsynchronously()); + } - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_ToMP4_YUV444p() + [SupportedOSPlatform("windows")] + [OsSpecificTestMethod(OsPlatforms.Windows)] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_ToMP4_Args_Pipe_DifferentPixelFormats_WindowsOnly() + { + Video_ToMP4_Args_Pipe_DifferentPixelFormats_Internal(PixelFormat.Format24bppRgb, + PixelFormat.Format32bppRgb); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_ToMP4_Args_Pipe_DifferentPixelFormats() + { + Video_ToMP4_Args_Pipe_DifferentPixelFormats_Internal(SKColorType.Rgb565, SKColorType.Bgra8888); + } + + private static void Video_ToMP4_Args_Pipe_DifferentPixelFormats_Internal(dynamic pixelFormatFrame1, dynamic pixelFormatFrame2) + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var frames = new List { - using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + BitmapSource.CreateVideoFrame(0, pixelFormatFrame1, 255, 255, 1, 0), BitmapSource.CreateVideoFrame(0, pixelFormatFrame2, 255, 255, 1, 0) + }; - var success = FFMpegArguments - .FromFileInput(TestResources.WebmVideo) - .OutputToFile(outputFile, false, opt => opt - .WithVideoCodec(VideoCodec.LibX264) - .ForcePixelFormat("yuv444p")) - .ProcessSynchronously(); - Assert.IsTrue(success); - var analysis = FFProbe.Analyse(outputFile); - Assert.IsTrue(analysis.VideoStreams.First().PixelFormat == "yuv444p"); - } + var videoFramesSource = new RawVideoPipeSource(frames); + Assert.ThrowsExactly(() => FFMpegArguments + .FromPipeInput(videoFramesSource) + .OutputToFile(outputFile, false, opt => opt + .WithVideoCodec(VideoCodec.LibX264)) + .ProcessSynchronously()); + } - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_ToMP4_Args() + [SupportedOSPlatform("windows")] + [OsSpecificTestMethod(OsPlatforms.Windows)] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public async Task Video_ToMP4_Args_Pipe_DifferentPixelFormats_WindowsOnly_Async() + { + await Video_ToMP4_Args_Pipe_DifferentPixelFormats_Internal_Async(PixelFormat.Format24bppRgb, + PixelFormat.Format32bppRgb); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public async Task Video_ToMP4_Args_Pipe_DifferentPixelFormats_Async() + { + await Video_ToMP4_Args_Pipe_DifferentPixelFormats_Internal_Async(SKColorType.Rgb565, SKColorType.Bgra8888); + } + + private static async Task Video_ToMP4_Args_Pipe_DifferentPixelFormats_Internal_Async(dynamic pixelFormatFrame1, dynamic pixelFormatFrame2) + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var frames = new List { - using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + BitmapSource.CreateVideoFrame(0, pixelFormatFrame1, 255, 255, 1, 0), BitmapSource.CreateVideoFrame(0, pixelFormatFrame2, 255, 255, 1, 0) + }; - var success = FFMpegArguments - .FromFileInput(TestResources.WebmVideo) - .OutputToFile(outputFile, false, opt => opt - .WithVideoCodec(VideoCodec.LibX264)) - .ProcessSynchronously(); - Assert.IsTrue(success); - } + var videoFramesSource = new RawVideoPipeSource(frames); + await Assert.ThrowsExactlyAsync(() => FFMpegArguments + .FromPipeInput(videoFramesSource) + .OutputToFile(outputFile, false, opt => opt + .WithVideoCodec(VideoCodec.LibX264)) + .ProcessAsynchronously()); + } - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_ToH265_MKV_Args() - { - using var outputFile = new TemporaryFile($"out.mkv"); + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_ToMP4_Args_StreamPipe() + { + using var input = File.OpenRead(TestResources.WebmVideo); + using var output = new TemporaryFile($"out{VideoType.Mp4.Extension}"); - var success = FFMpegArguments - .FromFileInput(TestResources.WebmVideo) - .OutputToFile(outputFile, false, opt => opt - .WithVideoCodec(VideoCodec.LibX265)) - .ProcessSynchronously(); - Assert.IsTrue(success); - } + var success = FFMpegArguments + .FromPipeInput(new StreamPipeSource(input)) + .OutputToFile(output, false, opt => opt + .WithVideoCodec(VideoCodec.LibX264)) + .ProcessSynchronously(); + Assert.IsTrue(success); + } - [SupportedOSPlatform("windows")] - [WindowsOnlyDataTestMethod, Timeout(BaseTimeoutMilliseconds)] - [DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)] - [DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)] - public void Video_ToMP4_Args_Pipe_WindowsOnly(System.Drawing.Imaging.PixelFormat pixelFormat) => Video_ToMP4_Args_Pipe_Internal(pixelFormat); - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - [DataRow(SkiaSharp.SKColorType.Rgb565)] - [DataRow(SkiaSharp.SKColorType.Bgra8888)] - public void Video_ToMP4_Args_Pipe(SkiaSharp.SKColorType pixelFormat) => Video_ToMP4_Args_Pipe_Internal(pixelFormat); - - private static void Video_ToMP4_Args_Pipe_Internal(dynamic pixelFormat) - { - using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); - - var videoFramesSource = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, pixelFormat, 256, 256)); - var success = FFMpegArguments - .FromPipeInput(videoFramesSource) - .OutputToFile(outputFile, false, opt => opt - .WithVideoCodec(VideoCodec.LibX264)) - .ProcessSynchronously(); - Assert.IsTrue(success); - } - - [SupportedOSPlatform("windows")] - [WindowsOnlyTestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_ToMP4_Args_Pipe_DifferentImageSizes_WindowsOnly() => Video_ToMP4_Args_Pipe_DifferentImageSizes_Internal(System.Drawing.Imaging.PixelFormat.Format24bppRgb); - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_ToMP4_Args_Pipe_DifferentImageSizes() => Video_ToMP4_Args_Pipe_DifferentImageSizes_Internal(SkiaSharp.SKColorType.Rgb565); - - private static void Video_ToMP4_Args_Pipe_DifferentImageSizes_Internal(dynamic pixelFormat) - { - using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); - - var frames = new List - { - BitmapSource.CreateVideoFrame(0, pixelFormat, 255, 255, 1, 0), - BitmapSource.CreateVideoFrame(0, pixelFormat, 256, 256, 1, 0) - }; - - var videoFramesSource = new RawVideoPipeSource(frames); - var ex = Assert.ThrowsException(() => FFMpegArguments - .FromPipeInput(videoFramesSource) - .OutputToFile(outputFile, false, opt => opt - .WithVideoCodec(VideoCodec.LibX264)) - .ProcessSynchronously()); - } - - [SupportedOSPlatform("windows")] - [WindowsOnlyTestMethod, Timeout(BaseTimeoutMilliseconds)] - public async Task Video_ToMP4_Args_Pipe_DifferentImageSizes_WindowsOnly_Async() => await Video_ToMP4_Args_Pipe_DifferentImageSizes_Internal_Async(System.Drawing.Imaging.PixelFormat.Format24bppRgb); - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public async Task Video_ToMP4_Args_Pipe_DifferentImageSizes_Async() => await Video_ToMP4_Args_Pipe_DifferentImageSizes_Internal_Async(SkiaSharp.SKColorType.Rgb565); - - private static async Task Video_ToMP4_Args_Pipe_DifferentImageSizes_Internal_Async(dynamic pixelFormat) - { - using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); - - var frames = new List - { - BitmapSource.CreateVideoFrame(0, pixelFormat, 255, 255, 1, 0), - BitmapSource.CreateVideoFrame(0, pixelFormat, 256, 256, 1, 0) - }; - - var videoFramesSource = new RawVideoPipeSource(frames); - var ex = await Assert.ThrowsExceptionAsync(() => FFMpegArguments - .FromPipeInput(videoFramesSource) - .OutputToFile(outputFile, false, opt => opt - .WithVideoCodec(VideoCodec.LibX264)) - .ProcessAsynchronously()); - } - - [SupportedOSPlatform("windows")] - [WindowsOnlyTestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_ToMP4_Args_Pipe_DifferentPixelFormats_WindowsOnly() => - Video_ToMP4_Args_Pipe_DifferentPixelFormats_Internal(System.Drawing.Imaging.PixelFormat.Format24bppRgb, System.Drawing.Imaging.PixelFormat.Format32bppRgb); - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_ToMP4_Args_Pipe_DifferentPixelFormats() => Video_ToMP4_Args_Pipe_DifferentPixelFormats_Internal(SkiaSharp.SKColorType.Rgb565, SkiaSharp.SKColorType.Bgra8888); - - private static void Video_ToMP4_Args_Pipe_DifferentPixelFormats_Internal(dynamic pixelFormatFrame1, dynamic pixelFormatFrame2) - { - using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); - - var frames = new List - { - BitmapSource.CreateVideoFrame(0, pixelFormatFrame1, 255, 255, 1, 0), - BitmapSource.CreateVideoFrame(0, pixelFormatFrame2, 255, 255, 1, 0) - }; - - var videoFramesSource = new RawVideoPipeSource(frames); - var ex = Assert.ThrowsException(() => FFMpegArguments - .FromPipeInput(videoFramesSource) - .OutputToFile(outputFile, false, opt => opt - .WithVideoCodec(VideoCodec.LibX264)) - .ProcessSynchronously()); - } - - [SupportedOSPlatform("windows")] - [WindowsOnlyTestMethod, Timeout(BaseTimeoutMilliseconds)] - public async Task Video_ToMP4_Args_Pipe_DifferentPixelFormats_WindowsOnly_Async() => - await Video_ToMP4_Args_Pipe_DifferentPixelFormats_Internal_Async(System.Drawing.Imaging.PixelFormat.Format24bppRgb, System.Drawing.Imaging.PixelFormat.Format32bppRgb); - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public async Task Video_ToMP4_Args_Pipe_DifferentPixelFormats_Async() => await Video_ToMP4_Args_Pipe_DifferentPixelFormats_Internal_Async(SkiaSharp.SKColorType.Rgb565, SkiaSharp.SKColorType.Bgra8888); - - private static async Task Video_ToMP4_Args_Pipe_DifferentPixelFormats_Internal_Async(dynamic pixelFormatFrame1, dynamic pixelFormatFrame2) - { - using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); - - var frames = new List - { - BitmapSource.CreateVideoFrame(0, pixelFormatFrame1, 255, 255, 1, 0), - BitmapSource.CreateVideoFrame(0, pixelFormatFrame2, 255, 255, 1, 0) - }; - - var videoFramesSource = new RawVideoPipeSource(frames); - var ex = await Assert.ThrowsExceptionAsync(() => FFMpegArguments - .FromPipeInput(videoFramesSource) - .OutputToFile(outputFile, false, opt => opt - .WithVideoCodec(VideoCodec.LibX264)) - .ProcessAsynchronously()); - } - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_ToMP4_Args_StreamPipe() - { - using var input = File.OpenRead(TestResources.WebmVideo); - using var output = new TemporaryFile($"out{VideoType.Mp4.Extension}"); - - var success = FFMpegArguments - .FromPipeInput(new StreamPipeSource(input)) - .OutputToFile(output, false, opt => opt - .WithVideoCodec(VideoCodec.LibX264)) - .ProcessSynchronously(); - Assert.IsTrue(success); - } - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public async Task Video_ToMP4_Args_StreamOutputPipe_Async_Failure() - { - await Assert.ThrowsExceptionAsync(async () => - { - await using var ms = new MemoryStream(); - var pipeSource = new StreamPipeSink(ms); - await FFMpegArguments - .FromFileInput(TestResources.Mp4Video) - .OutputToPipe(pipeSource, opt => opt.ForceFormat("mp4")) - .ProcessAsynchronously(); - }); - } - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_StreamFile_OutputToMemoryStream() - { - var output = new MemoryStream(); - - FFMpegArguments - .FromPipeInput(new StreamPipeSource(File.OpenRead(TestResources.WebmVideo)), opt => opt - .ForceFormat("webm")) - .OutputToPipe(new StreamPipeSink(output), opt => opt - .ForceFormat("mpegts")) - .ProcessSynchronously(); - - output.Position = 0; - var result = FFProbe.Analyse(output); - Console.WriteLine(result.Duration); - } - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_ToMP4_Args_StreamOutputPipe_Failure() - { - Assert.ThrowsException(() => - { - using var ms = new MemoryStream(); - FFMpegArguments - .FromFileInput(TestResources.Mp4Video) - .OutputToPipe(new StreamPipeSink(ms), opt => opt - .ForceFormat("mkv")) - .ProcessSynchronously(); - }); - } - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public async Task Video_ToMP4_Args_StreamOutputPipe_Async() + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public async Task Video_ToMP4_Args_StreamOutputPipe_Async_Failure() + { + await Assert.ThrowsExactlyAsync(async () => { await using var ms = new MemoryStream(); var pipeSource = new StreamPipeSink(ms); await FFMpegArguments .FromFileInput(TestResources.Mp4Video) - .OutputToPipe(pipeSource, opt => opt - .WithVideoCodec(VideoCodec.LibX264) - .ForceFormat("matroska")) + .OutputToPipe(pipeSource, opt => opt.ForceFormat("mp4")) .ProcessAsynchronously(); - } + }); + } - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public async Task TestDuplicateRun() + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_StreamFile_OutputToMemoryStream() + { + var output = new MemoryStream(); + + FFMpegArguments + .FromPipeInput(new StreamPipeSource(File.OpenRead(TestResources.WebmVideo)), opt => opt + .ForceFormat("webm")) + .OutputToPipe(new StreamPipeSink(output), opt => opt + .ForceFormat("mpegts")) + .ProcessSynchronously(); + + output.Position = 0; + var result = FFProbe.Analyse(output); + Console.WriteLine(result.Duration); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_ToMP4_Args_StreamOutputPipe_Failure() + { + Assert.ThrowsExactly(() => { + using var ms = new MemoryStream(); FFMpegArguments .FromFileInput(TestResources.Mp4Video) - .OutputToFile("temporary.mp4") + .OutputToPipe(new StreamPipeSink(ms), opt => opt + .ForceFormat("mkv")) .ProcessSynchronously(); + }); + } - await FFMpegArguments - .FromFileInput(TestResources.Mp4Video) - .OutputToFile("temporary.mp4") - .ProcessAsynchronously(); + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public async Task Video_ToMP4_Args_StreamOutputPipe_Async() + { + await using var ms = new MemoryStream(); + var pipeSource = new StreamPipeSink(ms); + await FFMpegArguments + .FromFileInput(TestResources.Mp4Video) + .OutputToPipe(pipeSource, opt => opt + .WithVideoCodec(VideoCodec.LibX264) + .ForceFormat("matroska")) + .ProcessAsynchronously(); + } - File.Delete("temporary.mp4"); - } + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public async Task TestDuplicateRun() + { + FFMpegArguments + .FromFileInput(TestResources.Mp4Video) + .OutputToFile("temporary.mp4") + .ProcessSynchronously(); - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public void TranscodeToMemoryStream_Success() + await FFMpegArguments + .FromFileInput(TestResources.Mp4Video) + .OutputToFile("temporary.mp4") + .ProcessAsynchronously(); + + File.Delete("temporary.mp4"); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void TranscodeToMemoryStream_Success() + { + using var output = new MemoryStream(); + var success = FFMpegArguments + .FromFileInput(TestResources.WebmVideo) + .OutputToPipe(new StreamPipeSink(output), opt => opt + .WithVideoCodec(VideoCodec.LibVpx) + .ForceFormat("matroska")) + .ProcessSynchronously(); + Assert.IsTrue(success); + + output.Position = 0; + var inputAnalysis = FFProbe.Analyse(TestResources.WebmVideo); + var outputAnalysis = FFProbe.Analyse(output); + Assert.AreEqual(inputAnalysis.Duration.TotalSeconds, outputAnalysis.Duration.TotalSeconds, 0.3); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_ToTS() + { + using var outputFile = new TemporaryFile($"out{VideoType.MpegTs.Extension}"); + + var success = FFMpegArguments + .FromFileInput(TestResources.Mp4Video) + .OutputToFile(outputFile, false) + .ProcessSynchronously(); + Assert.IsTrue(success); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_ToTS_Args() + { + using var outputFile = new TemporaryFile($"out{VideoType.MpegTs.Extension}"); + + var success = FFMpegArguments + .FromFileInput(TestResources.Mp4Video) + .OutputToFile(outputFile, false, opt => opt + .CopyChannel() + .WithBitStreamFilter(Channel.Video, Filter.H264_Mp4ToAnnexB) + .ForceFormat(VideoType.MpegTs)) + .ProcessSynchronously(); + Assert.IsTrue(success); + } + + [SupportedOSPlatform("windows")] + [OsSpecificTestMethod(OsPlatforms.Windows)] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + [DataRow(PixelFormat.Format24bppRgb)] + [DataRow(PixelFormat.Format32bppArgb)] + public async Task Video_ToTS_Args_Pipe_WindowsOnly(PixelFormat pixelFormat) + { + await Video_ToTS_Args_Pipe_Internal(pixelFormat); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + [DataRow(SKColorType.Rgb565)] + [DataRow(SKColorType.Bgra8888)] + public async Task Video_ToTS_Args_Pipe(SKColorType pixelFormat) + { + await Video_ToTS_Args_Pipe_Internal(pixelFormat); + } + + private static async Task Video_ToTS_Args_Pipe_Internal(dynamic pixelFormat) + { + using var output = new TemporaryFile($"out{VideoType.Ts.Extension}"); + var input = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, pixelFormat, 256, 256)); + + var success = await FFMpegArguments + .FromPipeInput(input) + .OutputToFile(output, false, opt => opt + .ForceFormat(VideoType.Ts)) + .ProcessAsynchronously(); + Assert.IsTrue(success); + + var analysis = await FFProbe.AnalyseAsync(output); + Assert.AreEqual(VideoType.Ts.Name, analysis.Format.FormatName); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public async Task Video_ToOGV_Resize() + { + using var outputFile = new TemporaryFile($"out{VideoType.Ogv.Extension}"); + var success = await FFMpegArguments + .FromFileInput(TestResources.Mp4Video) + .OutputToFile(outputFile, false, opt => opt + .Resize(200, 200) + .WithVideoCodec(VideoCodec.LibTheora)) + .ProcessAsynchronously(); + Assert.IsTrue(success); + } + + [SupportedOSPlatform("windows")] + [OsSpecificTestMethod(OsPlatforms.Windows)] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + [DataRow(SKColorType.Rgb565)] + [DataRow(SKColorType.Bgra8888)] + public void RawVideoPipeSource_Ogv_Scale(SKColorType pixelFormat) + { + using var outputFile = new TemporaryFile($"out{VideoType.Ogv.Extension}"); + var videoFramesSource = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, pixelFormat, 256, 256)); + + FFMpegArguments + .FromPipeInput(videoFramesSource) + .OutputToFile(outputFile, false, opt => opt + .WithVideoFilters(filterOptions => filterOptions + .Scale(VideoSize.Ed)) + .WithVideoCodec(VideoCodec.LibTheora)) + .ProcessSynchronously(); + + var analysis = FFProbe.Analyse(outputFile); + Assert.AreEqual((int)VideoSize.Ed, analysis.PrimaryVideoStream!.Width); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Scale_Mp4_Multithreaded() + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + + var success = FFMpegArguments + .FromFileInput(TestResources.Mp4Video) + .OutputToFile(outputFile, false, opt => opt + .UsingMultithreading(true) + .WithVideoCodec(VideoCodec.LibX264)) + .ProcessSynchronously(); + Assert.IsTrue(success); + } + + [SupportedOSPlatform("windows")] + [OsSpecificTestMethod(OsPlatforms.Windows)] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + [DataRow(PixelFormat.Format24bppRgb)] + [DataRow(PixelFormat.Format32bppArgb)] + // [DataRow(PixelFormat.Format48bppRgb)] + public void Video_ToMP4_Resize_Args_Pipe(PixelFormat pixelFormat) + { + Video_ToMP4_Resize_Args_Pipe_Internal(pixelFormat); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + [DataRow(SKColorType.Rgb565)] + [DataRow(SKColorType.Bgra8888)] + public void Video_ToMP4_Resize_Args_Pipe(SKColorType pixelFormat) + { + Video_ToMP4_Resize_Args_Pipe_Internal(pixelFormat); + } + + private static void Video_ToMP4_Resize_Args_Pipe_Internal(dynamic pixelFormat) + { + using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); + var videoFramesSource = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, pixelFormat, 256, 256)); + + var success = FFMpegArguments + .FromPipeInput(videoFramesSource) + .OutputToFile(outputFile, false, opt => opt + .WithVideoCodec(VideoCodec.LibX264)) + .ProcessSynchronously(); + Assert.IsTrue(success); + } + + [SupportedOSPlatform("windows")] + [OsSpecificTestMethod(OsPlatforms.Windows)] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_Snapshot_InMemory_SystemDrawingCommon() + { + using var bitmap = FFMpegImage.Snapshot(TestResources.Mp4Video); + + var input = FFProbe.Analyse(TestResources.Mp4Video); + Assert.AreEqual(input.PrimaryVideoStream!.Width, bitmap.Width); + Assert.AreEqual(input.PrimaryVideoStream.Height, bitmap.Height); + Assert.AreEqual(bitmap.RawFormat, ImageFormat.Png); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_Snapshot_InMemory_SkiaSharp() + { + using var bitmap = Extensions.SkiaSharp.FFMpegImage.Snapshot(TestResources.Mp4Video); + + var input = FFProbe.Analyse(TestResources.Mp4Video); + Assert.AreEqual(input.PrimaryVideoStream!.Width, bitmap.Width); + Assert.AreEqual(input.PrimaryVideoStream.Height, bitmap.Height); + // Note: The resulting ColorType is dependent on the execution environment and therefore not assessed, + // e.g. Bgra8888 on Windows and Rgba8888 on macOS. + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_Snapshot_Png_PersistSnapshot() + { + using var outputPath = new TemporaryFile("out.png"); + var input = FFProbe.Analyse(TestResources.Mp4Video); + + FFMpeg.Snapshot(TestResources.Mp4Video, outputPath); + + var analysis = FFProbe.Analyse(outputPath); + Assert.AreEqual(input.PrimaryVideoStream!.Width, analysis.PrimaryVideoStream!.Width); + Assert.AreEqual(input.PrimaryVideoStream.Height, analysis.PrimaryVideoStream!.Height); + Assert.AreEqual("png", analysis.PrimaryVideoStream!.CodecName); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_Snapshot_Jpg_PersistSnapshot() + { + using var outputPath = new TemporaryFile("out.jpg"); + var input = FFProbe.Analyse(TestResources.Mp4Video); + + FFMpeg.Snapshot(TestResources.Mp4Video, outputPath); + + var analysis = FFProbe.Analyse(outputPath); + Assert.AreEqual(input.PrimaryVideoStream!.Width, analysis.PrimaryVideoStream!.Width); + Assert.AreEqual(input.PrimaryVideoStream.Height, analysis.PrimaryVideoStream!.Height); + Assert.AreEqual("mjpeg", analysis.PrimaryVideoStream!.CodecName); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_Snapshot_Bmp_PersistSnapshot() + { + using var outputPath = new TemporaryFile("out.bmp"); + var input = FFProbe.Analyse(TestResources.Mp4Video); + + FFMpeg.Snapshot(TestResources.Mp4Video, outputPath); + + var analysis = FFProbe.Analyse(outputPath); + Assert.AreEqual(input.PrimaryVideoStream!.Width, analysis.PrimaryVideoStream!.Width); + Assert.AreEqual(input.PrimaryVideoStream.Height, analysis.PrimaryVideoStream!.Height); + Assert.AreEqual("bmp", analysis.PrimaryVideoStream!.CodecName); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_Snapshot_Webp_PersistSnapshot() + { + using var outputPath = new TemporaryFile("out.webp"); + var input = FFProbe.Analyse(TestResources.Mp4Video); + + FFMpeg.Snapshot(TestResources.Mp4Video, outputPath); + + var analysis = FFProbe.Analyse(outputPath); + Assert.AreEqual(input.PrimaryVideoStream!.Width, analysis.PrimaryVideoStream!.Width); + Assert.AreEqual(input.PrimaryVideoStream.Height, analysis.PrimaryVideoStream!.Height); + Assert.AreEqual("webp", analysis.PrimaryVideoStream!.CodecName); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_Snapshot_Exception_PersistSnapshot() + { + using var outputPath = new TemporaryFile("out.asd"); + + try { - using var output = new MemoryStream(); - var success = FFMpegArguments - .FromFileInput(TestResources.WebmVideo) - .OutputToPipe(new StreamPipeSink(output), opt => opt - .WithVideoCodec(VideoCodec.LibVpx) - .ForceFormat("matroska")) - .ProcessSynchronously(); - Assert.IsTrue(success); - - output.Position = 0; - var inputAnalysis = FFProbe.Analyse(TestResources.WebmVideo); - var outputAnalysis = FFProbe.Analyse(output); - Assert.AreEqual(inputAnalysis.Duration.TotalSeconds, outputAnalysis.Duration.TotalSeconds, 0.3); - } - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_ToTS() - { - using var outputFile = new TemporaryFile($"out{VideoType.MpegTs.Extension}"); - - var success = FFMpegArguments - .FromFileInput(TestResources.Mp4Video) - .OutputToFile(outputFile, false) - .ProcessSynchronously(); - Assert.IsTrue(success); - } - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_ToTS_Args() - { - using var outputFile = new TemporaryFile($"out{VideoType.MpegTs.Extension}"); - - var success = FFMpegArguments - .FromFileInput(TestResources.Mp4Video) - .OutputToFile(outputFile, false, opt => opt - .CopyChannel() - .WithBitStreamFilter(Channel.Video, Filter.H264_Mp4ToAnnexB) - .ForceFormat(VideoType.MpegTs)) - .ProcessSynchronously(); - Assert.IsTrue(success); - } - - [SupportedOSPlatform("windows")] - [WindowsOnlyDataTestMethod, Timeout(BaseTimeoutMilliseconds)] - [DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)] - [DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)] - public async Task Video_ToTS_Args_Pipe_WindowsOnly(System.Drawing.Imaging.PixelFormat pixelFormat) => await Video_ToTS_Args_Pipe_Internal(pixelFormat); - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - [DataRow(SkiaSharp.SKColorType.Rgb565)] - [DataRow(SkiaSharp.SKColorType.Bgra8888)] - public async Task Video_ToTS_Args_Pipe(SkiaSharp.SKColorType pixelFormat) => await Video_ToTS_Args_Pipe_Internal(pixelFormat); - - private static async Task Video_ToTS_Args_Pipe_Internal(dynamic pixelFormat) - { - using var output = new TemporaryFile($"out{VideoType.Ts.Extension}"); - var input = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, pixelFormat, 256, 256)); - - var success = await FFMpegArguments - .FromPipeInput(input) - .OutputToFile(output, false, opt => opt - .ForceFormat(VideoType.Ts)) - .ProcessAsynchronously(); - Assert.IsTrue(success); - - var analysis = await FFProbe.AnalyseAsync(output); - Assert.AreEqual(VideoType.Ts.Name, analysis.Format.FormatName); - } - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public async Task Video_ToOGV_Resize() - { - using var outputFile = new TemporaryFile($"out{VideoType.Ogv.Extension}"); - var success = await FFMpegArguments - .FromFileInput(TestResources.Mp4Video) - .OutputToFile(outputFile, false, opt => opt - .Resize(200, 200) - .WithVideoCodec(VideoCodec.LibTheora)) - .ProcessAsynchronously(); - Assert.IsTrue(success); - } - - [SupportedOSPlatform("windows")] - [WindowsOnlyDataTestMethod, Timeout(BaseTimeoutMilliseconds)] - [DataRow(SkiaSharp.SKColorType.Rgb565)] - [DataRow(SkiaSharp.SKColorType.Bgra8888)] - public void RawVideoPipeSource_Ogv_Scale(SkiaSharp.SKColorType pixelFormat) - { - using var outputFile = new TemporaryFile($"out{VideoType.Ogv.Extension}"); - var videoFramesSource = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, pixelFormat, 256, 256)); - - FFMpegArguments - .FromPipeInput(videoFramesSource) - .OutputToFile(outputFile, false, opt => opt - .WithVideoFilters(filterOptions => filterOptions - .Scale(VideoSize.Ed)) - .WithVideoCodec(VideoCodec.LibTheora)) - .ProcessSynchronously(); - - var analysis = FFProbe.Analyse(outputFile); - Assert.AreEqual((int)VideoSize.Ed, analysis.PrimaryVideoStream!.Width); - } - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Scale_Mp4_Multithreaded() - { - using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); - - var success = FFMpegArguments - .FromFileInput(TestResources.Mp4Video) - .OutputToFile(outputFile, false, opt => opt - .UsingMultithreading(true) - .WithVideoCodec(VideoCodec.LibX264)) - .ProcessSynchronously(); - Assert.IsTrue(success); - } - - [SupportedOSPlatform("windows")] - [WindowsOnlyDataTestMethod, Timeout(BaseTimeoutMilliseconds)] - [DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)] - [DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)] - // [DataRow(PixelFormat.Format48bppRgb)] - public void Video_ToMP4_Resize_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat) => Video_ToMP4_Resize_Args_Pipe_Internal(pixelFormat); - - [DataTestMethod, Timeout(BaseTimeoutMilliseconds)] - [DataRow(SkiaSharp.SKColorType.Rgb565)] - [DataRow(SkiaSharp.SKColorType.Bgra8888)] - public void Video_ToMP4_Resize_Args_Pipe(SkiaSharp.SKColorType pixelFormat) => Video_ToMP4_Resize_Args_Pipe_Internal(pixelFormat); - - private static void Video_ToMP4_Resize_Args_Pipe_Internal(dynamic pixelFormat) - { - using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); - var videoFramesSource = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, pixelFormat, 256, 256)); - - var success = FFMpegArguments - .FromPipeInput(videoFramesSource) - .OutputToFile(outputFile, false, opt => opt - .WithVideoCodec(VideoCodec.LibX264)) - .ProcessSynchronously(); - Assert.IsTrue(success); - } - - [SupportedOSPlatform("windows")] - [WindowsOnlyTestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_Snapshot_InMemory_SystemDrawingCommon() - { - using var bitmap = Extensions.System.Drawing.Common.FFMpegImage.Snapshot(TestResources.Mp4Video); - - var input = FFProbe.Analyse(TestResources.Mp4Video); - Assert.AreEqual(input.PrimaryVideoStream!.Width, bitmap.Width); - Assert.AreEqual(input.PrimaryVideoStream.Height, bitmap.Height); - Assert.AreEqual(bitmap.RawFormat, ImageFormat.Png); - } - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_Snapshot_InMemory_SkiaSharp() - { - using var bitmap = Extensions.SkiaSharp.FFMpegImage.Snapshot(TestResources.Mp4Video); - - var input = FFProbe.Analyse(TestResources.Mp4Video); - Assert.AreEqual(input.PrimaryVideoStream!.Width, bitmap.Width); - Assert.AreEqual(input.PrimaryVideoStream.Height, bitmap.Height); - // Note: The resulting ColorType is dependent on the execution environment and therefore not assessed, - // e.g. Bgra8888 on Windows and Rgba8888 on macOS. - } - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_Snapshot_PersistSnapshot() - { - using var outputPath = new TemporaryFile("out.png"); - var input = FFProbe.Analyse(TestResources.Mp4Video); - FFMpeg.Snapshot(TestResources.Mp4Video, outputPath); - - var analysis = FFProbe.Analyse(outputPath); - Assert.AreEqual(input.PrimaryVideoStream!.Width, analysis.PrimaryVideoStream!.Width); - Assert.AreEqual(input.PrimaryVideoStream.Height, analysis.PrimaryVideoStream!.Height); - Assert.AreEqual("png", analysis.PrimaryVideoStream!.CodecName); } - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_Snapshot_Rotated_PersistSnapshot() + catch (Exception ex) { - using var outputPath = new TemporaryFile("out.png"); - - var size = new Size(360, 0); // half the size of original video, keeping height 0 for keeping aspect ratio - FFMpeg.Snapshot(TestResources.Mp4VideoRotationNegative, outputPath, size); - - var analysis = FFProbe.Analyse(outputPath); - Assert.AreEqual(size.Width, analysis.PrimaryVideoStream!.Width); - Assert.AreEqual(1280 / 2, analysis.PrimaryVideoStream!.Height); - Assert.AreEqual(0, analysis.PrimaryVideoStream!.Rotation); - Assert.AreEqual("png", analysis.PrimaryVideoStream!.CodecName); - } - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_GifSnapshot_PersistSnapshot() - { - using var outputPath = new TemporaryFile("out.gif"); - var input = FFProbe.Analyse(TestResources.Mp4Video); - - FFMpeg.GifSnapshot(TestResources.Mp4Video, outputPath, captureTime: TimeSpan.FromSeconds(0)); - - var analysis = FFProbe.Analyse(outputPath); - Assert.AreNotEqual(input.PrimaryVideoStream!.Width, analysis.PrimaryVideoStream!.Width); - Assert.AreNotEqual(input.PrimaryVideoStream.Height, analysis.PrimaryVideoStream!.Height); - Assert.AreEqual("gif", analysis.PrimaryVideoStream!.CodecName); - } - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_GifSnapshot_PersistSnapshot_SizeSupplied() - { - using var outputPath = new TemporaryFile("out.gif"); - var input = FFProbe.Analyse(TestResources.Mp4Video); - var desiredGifSize = new Size(320, 240); - - FFMpeg.GifSnapshot(TestResources.Mp4Video, outputPath, desiredGifSize, captureTime: TimeSpan.FromSeconds(0)); - - var analysis = FFProbe.Analyse(outputPath); - Assert.AreNotEqual(input.PrimaryVideoStream!.Width, desiredGifSize.Width); - Assert.AreNotEqual(input.PrimaryVideoStream.Height, desiredGifSize.Height); - Assert.AreEqual("gif", analysis.PrimaryVideoStream!.CodecName); - } - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public async Task Video_GifSnapshot_PersistSnapshotAsync() - { - using var outputPath = new TemporaryFile("out.gif"); - var input = FFProbe.Analyse(TestResources.Mp4Video); - - await FFMpeg.GifSnapshotAsync(TestResources.Mp4Video, outputPath, captureTime: TimeSpan.FromSeconds(0)); - - var analysis = FFProbe.Analyse(outputPath); - Assert.AreNotEqual(input.PrimaryVideoStream!.Width, analysis.PrimaryVideoStream!.Width); - Assert.AreNotEqual(input.PrimaryVideoStream.Height, analysis.PrimaryVideoStream!.Height); - Assert.AreEqual("gif", analysis.PrimaryVideoStream!.CodecName); - } - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public async Task Video_GifSnapshot_PersistSnapshotAsync_SizeSupplied() - { - using var outputPath = new TemporaryFile("out.gif"); - var input = FFProbe.Analyse(TestResources.Mp4Video); - var desiredGifSize = new Size(320, 240); - - await FFMpeg.GifSnapshotAsync(TestResources.Mp4Video, outputPath, desiredGifSize, captureTime: TimeSpan.FromSeconds(0)); - - var analysis = FFProbe.Analyse(outputPath); - Assert.AreNotEqual(input.PrimaryVideoStream!.Width, desiredGifSize.Width); - Assert.AreNotEqual(input.PrimaryVideoStream.Height, desiredGifSize.Height); - Assert.AreEqual("gif", analysis.PrimaryVideoStream!.CodecName); - } - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_Join() - { - using var inputCopy = new TemporaryFile("copy-input.mp4"); - File.Copy(TestResources.Mp4Video, inputCopy); - - using var outputPath = new TemporaryFile("out.mp4"); - var input = FFProbe.Analyse(TestResources.Mp4Video); - var success = FFMpeg.Join(outputPath, TestResources.Mp4Video, inputCopy); - Assert.IsTrue(success); - Assert.IsTrue(File.Exists(outputPath)); - - var expectedDuration = input.Duration * 2; - var result = FFProbe.Analyse(outputPath); - Assert.AreEqual(expectedDuration.Days, result.Duration.Days); - Assert.AreEqual(expectedDuration.Hours, result.Duration.Hours); - Assert.AreEqual(expectedDuration.Minutes, result.Duration.Minutes); - Assert.AreEqual(expectedDuration.Seconds, result.Duration.Seconds); - Assert.AreEqual(input.PrimaryVideoStream!.Height, result.PrimaryVideoStream!.Height); - Assert.AreEqual(input.PrimaryVideoStream.Width, result.PrimaryVideoStream.Width); - } - - [TestMethod, Timeout(2 * BaseTimeoutMilliseconds)] - public void Video_Join_Image_Sequence() - { - var imageSet = new List(); - Directory.EnumerateFiles(TestResources.ImageCollection, "*.png") - .ToList() - .ForEach(file => - { - for (var i = 0; i < 5; i++) - { - imageSet.Add(file); - } - }); - var imageAnalysis = FFProbe.Analyse(imageSet.First()); - - using var outputFile = new TemporaryFile("out.mp4"); - var success = FFMpeg.JoinImageSequence(outputFile, frameRate: 10, images: imageSet.ToArray()); - Assert.IsTrue(success); - var result = FFProbe.Analyse(outputFile); - - Assert.AreEqual(1, result.Duration.Seconds); - Assert.AreEqual(imageAnalysis.PrimaryVideoStream!.Width, result.PrimaryVideoStream!.Width); - Assert.AreEqual(imageAnalysis.PrimaryVideoStream!.Height, result.PrimaryVideoStream.Height); - } - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_With_Only_Audio_Should_Extract_Metadata() - { - var video = FFProbe.Analyse(TestResources.Mp4WithoutVideo); - Assert.AreEqual(null, video.PrimaryVideoStream); - Assert.AreEqual("aac", video.PrimaryAudioStream!.CodecName); - Assert.AreEqual(10, video.Duration.TotalSeconds, 0.5); - } - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_Duration() - { - var video = FFProbe.Analyse(TestResources.Mp4Video); - using var outputFile = new TemporaryFile("out.mp4"); - - FFMpegArguments - .FromFileInput(TestResources.Mp4Video) - .OutputToFile(outputFile, false, opt => opt.WithDuration(TimeSpan.FromSeconds(video.Duration.TotalSeconds - 2))) - .ProcessSynchronously(); - - Assert.IsTrue(File.Exists(outputFile)); - var outputVideo = FFProbe.Analyse(outputFile); - - Assert.AreEqual(video.Duration.Days, outputVideo.Duration.Days); - Assert.AreEqual(video.Duration.Hours, outputVideo.Duration.Hours); - Assert.AreEqual(video.Duration.Minutes, outputVideo.Duration.Minutes); - Assert.AreEqual(video.Duration.Seconds - 2, outputVideo.Duration.Seconds); - } - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_UpdatesProgress() - { - using var outputFile = new TemporaryFile("out.mp4"); - - var percentageDone = 0.0; - var timeDone = TimeSpan.Zero; - var analysis = FFProbe.Analyse(TestResources.Mp4Video); - - void OnPercentageProgess(double percentage) - { - if (percentage < 100) - { - percentageDone = percentage; - } - } - - void OnTimeProgess(TimeSpan time) - { - if (time < analysis.Duration) - { - timeDone = time; - } - } - - var success = FFMpegArguments - .FromFileInput(TestResources.Mp4Video) - .OutputToFile(outputFile, false, opt => opt - .WithDuration(analysis.Duration)) - .NotifyOnProgress(OnPercentageProgess, analysis.Duration) - .NotifyOnProgress(OnTimeProgess) - .ProcessSynchronously(); - - Assert.IsTrue(success); - Assert.IsTrue(File.Exists(outputFile)); - Assert.AreNotEqual(0.0, percentageDone); - Assert.AreNotEqual(100.0, percentageDone); - Assert.AreNotEqual(TimeSpan.Zero, timeDone); - Assert.AreNotEqual(analysis.Duration, timeDone); - } - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_OutputsData() - { - using var outputFile = new TemporaryFile("out.mp4"); - var dataReceived = false; - - GlobalFFOptions.Configure(opt => opt.Encoding = Encoding.UTF8); - var success = FFMpegArguments - .FromFileInput(TestResources.Mp4Video) - .WithGlobalOptions(options => options - .WithVerbosityLevel(VerbosityLevel.Info)) - .OutputToFile(outputFile, false, opt => opt - .WithDuration(TimeSpan.FromSeconds(2))) - .NotifyOnError(_ => dataReceived = true) - .ProcessSynchronously(); - - Assert.IsTrue(dataReceived); - Assert.IsTrue(success); - Assert.IsTrue(File.Exists(outputFile)); - } - - [SupportedOSPlatform("windows")] - [WindowsOnlyTestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_TranscodeInMemory_WindowsOnly() => Video_TranscodeInMemory_Internal(System.Drawing.Imaging.PixelFormat.Format24bppRgb); - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_TranscodeInMemory() => Video_TranscodeInMemory_Internal(SkiaSharp.SKColorType.Rgb565); - - private static void Video_TranscodeInMemory_Internal(dynamic pixelFormat) - { - using var resStream = new MemoryStream(); - var reader = new StreamPipeSink(resStream); - var writer = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, pixelFormat, 128, 128)); - - FFMpegArguments - .FromPipeInput(writer) - .OutputToPipe(reader, opt => opt - .WithVideoCodec("vp9") - .ForceFormat("webm")) - .ProcessSynchronously(); - - resStream.Position = 0; - var vi = FFProbe.Analyse(resStream); - Assert.AreEqual(vi.PrimaryVideoStream!.Width, 128); - Assert.AreEqual(vi.PrimaryVideoStream.Height, 128); - } - - [TestMethod, Timeout(2 * BaseTimeoutMilliseconds)] - public void Video_TranscodeToMemory() - { - using var memoryStream = new MemoryStream(); - - FFMpegArguments - .FromFileInput(TestResources.WebmVideo) - .OutputToPipe(new StreamPipeSink(memoryStream), opt => opt - .WithVideoCodec("vp9") - .ForceFormat("webm")) - .ProcessSynchronously(); - - memoryStream.Position = 0; - var vi = FFProbe.Analyse(memoryStream); - Assert.AreEqual(vi.PrimaryVideoStream!.Width, 640); - Assert.AreEqual(vi.PrimaryVideoStream.Height, 360); - } - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public async Task Video_Cancel_Async() - { - using var outputFile = new TemporaryFile("out.mp4"); - - var task = FFMpegArguments - .FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args - .WithCustomArgument("-re") - .ForceFormat("lavfi")) - .OutputToFile(outputFile, false, opt => opt - .WithAudioCodec(AudioCodec.Aac) - .WithVideoCodec(VideoCodec.LibX264) - .WithSpeedPreset(Speed.VeryFast)) - .CancellableThrough(out var cancel) - .ProcessAsynchronously(false); - - await Task.Delay(300); - cancel(); - - var result = await task; - - Assert.IsFalse(result); - } - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_Cancel() - { - using var outputFile = new TemporaryFile("out.mp4"); - var task = FFMpegArguments - .FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args - .WithCustomArgument("-re") - .ForceFormat("lavfi")) - .OutputToFile(outputFile, false, opt => opt - .WithAudioCodec(AudioCodec.Aac) - .WithVideoCodec(VideoCodec.LibX264) - .WithSpeedPreset(Speed.VeryFast)) - .CancellableThrough(out var cancel); - - Task.Delay(300).ContinueWith((_) => cancel()); - - var result = task.ProcessSynchronously(false); - - Assert.IsFalse(result); - } - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public async Task Video_Cancel_Async_With_Timeout() - { - using var outputFile = new TemporaryFile("out.mp4"); - - var task = FFMpegArguments - .FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args - .WithCustomArgument("-re") - .ForceFormat("lavfi")) - .OutputToFile(outputFile, false, opt => opt - .WithAudioCodec(AudioCodec.Aac) - .WithVideoCodec(VideoCodec.LibX264) - .WithSpeedPreset(Speed.VeryFast)) - .CancellableThrough(out var cancel, 10000) - .ProcessAsynchronously(false); - - await Task.Delay(300); - cancel(); - - await task; - - var outputInfo = await FFProbe.AnalyseAsync(outputFile); - - Assert.IsNotNull(outputInfo); - Assert.AreEqual(320, outputInfo.PrimaryVideoStream!.Width); - Assert.AreEqual(240, outputInfo.PrimaryVideoStream.Height); - Assert.AreEqual("h264", outputInfo.PrimaryVideoStream.CodecName); - Assert.AreEqual("aac", outputInfo.PrimaryAudioStream!.CodecName); - } - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public async Task Video_Cancel_CancellationToken_Async() - { - using var outputFile = new TemporaryFile("out.mp4"); - - var cts = new CancellationTokenSource(); - - var task = FFMpegArguments - .FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args - .WithCustomArgument("-re") - .ForceFormat("lavfi")) - .OutputToFile(outputFile, false, opt => opt - .WithAudioCodec(AudioCodec.Aac) - .WithVideoCodec(VideoCodec.LibX264) - .WithSpeedPreset(Speed.VeryFast)) - .CancellableThrough(cts.Token) - .ProcessAsynchronously(false); - - cts.CancelAfter(300); - - var result = await task; - - Assert.IsFalse(result); - } - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public async Task Video_Cancel_CancellationToken_Async_Throws() - { - using var outputFile = new TemporaryFile("out.mp4"); - - var cts = new CancellationTokenSource(); - - var task = FFMpegArguments - .FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args - .WithCustomArgument("-re") - .ForceFormat("lavfi")) - .OutputToFile(outputFile, false, opt => opt - .WithAudioCodec(AudioCodec.Aac) - .WithVideoCodec(VideoCodec.LibX264) - .WithSpeedPreset(Speed.VeryFast)) - .CancellableThrough(cts.Token) - .ProcessAsynchronously(); - - cts.CancelAfter(300); - - await Assert.ThrowsExceptionAsync(() => task); - } - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_Cancel_CancellationToken_Throws() - { - using var outputFile = new TemporaryFile("out.mp4"); - - var cts = new CancellationTokenSource(); - - var task = FFMpegArguments - .FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args - .WithCustomArgument("-re") - .ForceFormat("lavfi")) - .OutputToFile(outputFile, false, opt => opt - .WithAudioCodec(AudioCodec.Aac) - .WithVideoCodec(VideoCodec.LibX264) - .WithSpeedPreset(Speed.VeryFast)) - .CancellableThrough(cts.Token); - - cts.CancelAfter(300); - - Assert.ThrowsException(() => task.ProcessSynchronously()); - } - - [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public async Task Video_Cancel_CancellationToken_Async_With_Timeout() - { - using var outputFile = new TemporaryFile("out.mp4"); - - var cts = new CancellationTokenSource(); - - var task = FFMpegArguments - .FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args - .WithCustomArgument("-re") - .ForceFormat("lavfi")) - .OutputToFile(outputFile, false, opt => opt - .WithAudioCodec(AudioCodec.Aac) - .WithVideoCodec(VideoCodec.LibX264) - .WithSpeedPreset(Speed.VeryFast)) - .CancellableThrough(cts.Token, 8000) - .ProcessAsynchronously(false); - - cts.CancelAfter(300); - - await task; - - var outputInfo = await FFProbe.AnalyseAsync(outputFile); - - Assert.IsNotNull(outputInfo); - Assert.AreEqual(320, outputInfo.PrimaryVideoStream!.Width); - Assert.AreEqual(240, outputInfo.PrimaryVideoStream.Height); - Assert.AreEqual("h264", outputInfo.PrimaryVideoStream.CodecName); - Assert.AreEqual("aac", outputInfo.PrimaryAudioStream!.CodecName); + Assert.IsTrue(ex is ArgumentException); } } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_Snapshot_Rotated_PersistSnapshot() + { + using var outputPath = new TemporaryFile("out.png"); + + var size = new Size(360, 0); // half the size of original video, keeping height 0 for keeping aspect ratio + FFMpeg.Snapshot(TestResources.Mp4VideoRotationNegative, outputPath, size); + + var analysis = FFProbe.Analyse(outputPath); + Assert.AreEqual(size.Width, analysis.PrimaryVideoStream!.Width); + Assert.AreEqual(1280 / 2, analysis.PrimaryVideoStream!.Height); + Assert.AreEqual(0, analysis.PrimaryVideoStream!.Rotation); + Assert.AreEqual("png", analysis.PrimaryVideoStream!.CodecName); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_GifSnapshot_PersistSnapshot() + { + using var outputPath = new TemporaryFile("out.gif"); + var input = FFProbe.Analyse(TestResources.Mp4Video); + + FFMpeg.GifSnapshot(TestResources.Mp4Video, outputPath, captureTime: TimeSpan.FromSeconds(0)); + + var analysis = FFProbe.Analyse(outputPath); + Assert.AreNotEqual(input.PrimaryVideoStream!.Width, analysis.PrimaryVideoStream!.Width); + Assert.AreNotEqual(input.PrimaryVideoStream.Height, analysis.PrimaryVideoStream!.Height); + Assert.AreEqual("gif", analysis.PrimaryVideoStream!.CodecName); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_GifSnapshot_PersistSnapshot_SizeSupplied() + { + using var outputPath = new TemporaryFile("out.gif"); + var input = FFProbe.Analyse(TestResources.Mp4Video); + var desiredGifSize = new Size(320, 240); + + FFMpeg.GifSnapshot(TestResources.Mp4Video, outputPath, desiredGifSize, TimeSpan.FromSeconds(0)); + + var analysis = FFProbe.Analyse(outputPath); + Assert.AreNotEqual(input.PrimaryVideoStream!.Width, desiredGifSize.Width); + Assert.AreNotEqual(input.PrimaryVideoStream.Height, desiredGifSize.Height); + Assert.AreEqual("gif", analysis.PrimaryVideoStream!.CodecName); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public async Task Video_GifSnapshot_PersistSnapshotAsync() + { + using var outputPath = new TemporaryFile("out.gif"); + var input = FFProbe.Analyse(TestResources.Mp4Video); + + await FFMpeg.GifSnapshotAsync(TestResources.Mp4Video, outputPath, captureTime: TimeSpan.FromSeconds(0)); + + var analysis = FFProbe.Analyse(outputPath); + Assert.AreNotEqual(input.PrimaryVideoStream!.Width, analysis.PrimaryVideoStream!.Width); + Assert.AreNotEqual(input.PrimaryVideoStream.Height, analysis.PrimaryVideoStream!.Height); + Assert.AreEqual("gif", analysis.PrimaryVideoStream!.CodecName); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public async Task Video_GifSnapshot_PersistSnapshotAsync_SizeSupplied() + { + using var outputPath = new TemporaryFile("out.gif"); + var input = FFProbe.Analyse(TestResources.Mp4Video); + var desiredGifSize = new Size(320, 240); + + await FFMpeg.GifSnapshotAsync(TestResources.Mp4Video, outputPath, desiredGifSize, TimeSpan.FromSeconds(0)); + + var analysis = FFProbe.Analyse(outputPath); + Assert.AreNotEqual(input.PrimaryVideoStream!.Width, desiredGifSize.Width); + Assert.AreNotEqual(input.PrimaryVideoStream.Height, desiredGifSize.Height); + Assert.AreEqual("gif", analysis.PrimaryVideoStream!.CodecName); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_Join() + { + using var inputCopy = new TemporaryFile("copy-input.mp4"); + File.Copy(TestResources.Mp4Video, inputCopy); + + using var outputPath = new TemporaryFile("out.mp4"); + var input = FFProbe.Analyse(TestResources.Mp4Video); + var success = FFMpeg.Join(outputPath, TestResources.Mp4Video, inputCopy); + Assert.IsTrue(success); + Assert.IsTrue(File.Exists(outputPath)); + + var expectedDuration = input.Duration * 2; + var result = FFProbe.Analyse(outputPath); + Assert.AreEqual(expectedDuration.Days, result.Duration.Days); + Assert.AreEqual(expectedDuration.Hours, result.Duration.Hours); + Assert.AreEqual(expectedDuration.Minutes, result.Duration.Minutes); + Assert.AreEqual(expectedDuration.Seconds, result.Duration.Seconds); + Assert.AreEqual(input.PrimaryVideoStream!.Height, result.PrimaryVideoStream!.Height); + Assert.AreEqual(input.PrimaryVideoStream.Width, result.PrimaryVideoStream.Width); + } + + [TestMethod] + [Timeout(2 * BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_Join_Image_Sequence() + { + var imageSet = new List(); + Directory.EnumerateFiles(TestResources.ImageCollection, "*.png") + .ToList() + .ForEach(file => + { + for (var i = 0; i < 5; i++) + { + imageSet.Add(file); + } + }); + var imageAnalysis = FFProbe.Analyse(imageSet.First()); + + using var outputFile = new TemporaryFile("out.mp4"); + var success = FFMpeg.JoinImageSequence(outputFile, 10, imageSet.ToArray()); + Assert.IsTrue(success); + var result = FFProbe.Analyse(outputFile); + + Assert.AreEqual(1, result.Duration.Seconds); + Assert.AreEqual(imageAnalysis.PrimaryVideoStream!.Width, result.PrimaryVideoStream!.Width); + Assert.AreEqual(imageAnalysis.PrimaryVideoStream!.Height, result.PrimaryVideoStream.Height); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_With_Only_Audio_Should_Extract_Metadata() + { + var video = FFProbe.Analyse(TestResources.Mp4WithoutVideo); + Assert.IsNull(video.PrimaryVideoStream); + Assert.AreEqual("aac", video.PrimaryAudioStream!.CodecName); + Assert.AreEqual(10, video.Duration.TotalSeconds, 0.5); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_Duration() + { + var video = FFProbe.Analyse(TestResources.Mp4Video); + using var outputFile = new TemporaryFile("out.mp4"); + + FFMpegArguments + .FromFileInput(TestResources.Mp4Video) + .OutputToFile(outputFile, false, opt => opt.WithDuration(TimeSpan.FromSeconds(video.Duration.TotalSeconds - 2))) + .ProcessSynchronously(); + + Assert.IsTrue(File.Exists(outputFile)); + var outputVideo = FFProbe.Analyse(outputFile); + + Assert.AreEqual(video.Duration.Days, outputVideo.Duration.Days); + Assert.AreEqual(video.Duration.Hours, outputVideo.Duration.Hours); + Assert.AreEqual(video.Duration.Minutes, outputVideo.Duration.Minutes); + Assert.AreEqual(video.Duration.Seconds - 2, outputVideo.Duration.Seconds); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_UpdatesProgress() + { + using var outputFile = new TemporaryFile("out.mp4"); + + var percentageDone = 0.0; + var timeDone = TimeSpan.Zero; + var analysis = FFProbe.Analyse(TestResources.Mp4Video); + + void OnPercentageProgess(double percentage) + { + if (percentage < 100) + { + percentageDone = percentage; + } + } + + void OnTimeProgess(TimeSpan time) + { + if (time < analysis.Duration) + { + timeDone = time; + } + } + + var success = FFMpegArguments + .FromFileInput(TestResources.Mp4Video) + .OutputToFile(outputFile, false, opt => opt + .WithDuration(analysis.Duration)) + .NotifyOnProgress(OnPercentageProgess, analysis.Duration) + .NotifyOnProgress(OnTimeProgess) + .ProcessSynchronously(); + + Assert.IsTrue(success); + Assert.IsTrue(File.Exists(outputFile)); + Assert.AreNotEqual(0.0, percentageDone); + Assert.AreNotEqual(100.0, percentageDone); + Assert.AreNotEqual(TimeSpan.Zero, timeDone); + Assert.AreNotEqual(analysis.Duration, timeDone); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_OutputsData() + { + using var outputFile = new TemporaryFile("out.mp4"); + var dataReceived = false; + + var success = FFMpegArguments + .FromFileInput(TestResources.Mp4Video) + .WithGlobalOptions(options => options + .WithVerbosityLevel(VerbosityLevel.Info)) + .OutputToFile(outputFile, false, opt => opt + .WithDuration(TimeSpan.FromSeconds(2))) + .NotifyOnError(_ => dataReceived = true) + .Configure(opt => opt.Encoding = Encoding.UTF8) + .ProcessSynchronously(); + + Assert.IsTrue(dataReceived); + Assert.IsTrue(success); + Assert.IsTrue(File.Exists(outputFile)); + } + + [SupportedOSPlatform("windows")] + [OsSpecificTestMethod(OsPlatforms.Windows)] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_TranscodeInMemory_WindowsOnly() + { + Video_TranscodeInMemory_Internal(PixelFormat.Format24bppRgb); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_TranscodeInMemory() + { + Video_TranscodeInMemory_Internal(SKColorType.Rgb565); + } + + private static void Video_TranscodeInMemory_Internal(dynamic pixelFormat) + { + using var resStream = new MemoryStream(); + var reader = new StreamPipeSink(resStream); + var writer = new RawVideoPipeSource(BitmapSource.CreateBitmaps(128, pixelFormat, 128, 128)); + + FFMpegArguments + .FromPipeInput(writer) + .OutputToPipe(reader, opt => opt + .WithVideoCodec("vp9") + .ForceFormat("webm")) + .ProcessSynchronously(); + + resStream.Position = 0; + var vi = FFProbe.Analyse(resStream); + Assert.AreEqual(128, vi.PrimaryVideoStream!.Width); + Assert.AreEqual(128, vi.PrimaryVideoStream.Height); + } + + [TestMethod] + [Timeout(2 * BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_TranscodeToMemory() + { + using var memoryStream = new MemoryStream(); + + FFMpegArguments + .FromFileInput(TestResources.WebmVideo) + .OutputToPipe(new StreamPipeSink(memoryStream), opt => opt + .WithVideoCodec("vp9") + .ForceFormat("webm")) + .ProcessSynchronously(); + + memoryStream.Position = 0; + var vi = FFProbe.Analyse(memoryStream); + Assert.AreEqual(640, vi.PrimaryVideoStream!.Width); + Assert.AreEqual(360, vi.PrimaryVideoStream.Height); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public async Task Video_Cancel_Async() + { + using var outputFile = new TemporaryFile("out.mp4"); + + var task = FFMpegArguments + .FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args + .WithCustomArgument("-re") + .ForceFormat("lavfi")) + .OutputToFile(outputFile, false, opt => opt + .WithAudioCodec(AudioCodec.Aac) + .WithVideoCodec(VideoCodec.LibX264) + .WithSpeedPreset(Speed.VeryFast)) + .CancellableThrough(out var cancel) + .ProcessAsynchronously(false); + + await Task.Delay(300, TestContext.CancellationToken); + cancel(); + + var result = await task; + + Assert.IsFalse(result); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_Cancel() + { + using var outputFile = new TemporaryFile("out.mp4"); + var task = FFMpegArguments + .FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args + .WithCustomArgument("-re") + .ForceFormat("lavfi")) + .OutputToFile(outputFile, false, opt => opt + .WithAudioCodec(AudioCodec.Aac) + .WithVideoCodec(VideoCodec.LibX264) + .WithSpeedPreset(Speed.VeryFast)) + .CancellableThrough(out var cancel); + + Task.Delay(300, TestContext.CancellationToken).ContinueWith(_ => cancel(), TestContext.CancellationToken); + + var result = task.ProcessSynchronously(false); + + Assert.IsFalse(result); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public async Task Video_Cancel_Async_With_Timeout() + { + using var outputFile = new TemporaryFile("out.mp4"); + + var task = FFMpegArguments + .FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args + .WithCustomArgument("-re") + .ForceFormat("lavfi")) + .OutputToFile(outputFile, false, opt => opt + .WithAudioCodec(AudioCodec.Aac) + .WithVideoCodec(VideoCodec.LibX264) + .WithSpeedPreset(Speed.VeryFast)) + .CancellableThrough(out var cancel, 10000) + .ProcessAsynchronously(false); + + await Task.Delay(300, TestContext.CancellationToken); + cancel(); + + await task; + + var outputInfo = await FFProbe.AnalyseAsync(outputFile, cancellationToken: TestContext.CancellationToken); + + Assert.IsNotNull(outputInfo); + Assert.AreEqual(320, outputInfo.PrimaryVideoStream!.Width); + Assert.AreEqual(240, outputInfo.PrimaryVideoStream.Height); + Assert.AreEqual("h264", outputInfo.PrimaryVideoStream.CodecName); + Assert.AreEqual("aac", outputInfo.PrimaryAudioStream!.CodecName); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public async Task Video_Cancel_CancellationToken_Async() + { + using var outputFile = new TemporaryFile("out.mp4"); + + var cts = new CancellationTokenSource(); + + var task = FFMpegArguments + .FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args + .WithCustomArgument("-re") + .ForceFormat("lavfi")) + .OutputToFile(outputFile, false, opt => opt + .WithAudioCodec(AudioCodec.Aac) + .WithVideoCodec(VideoCodec.LibX264) + .WithSpeedPreset(Speed.VeryFast)) + .CancellableThrough(cts.Token) + .ProcessAsynchronously(false); + + cts.CancelAfter(300); + + var result = await task; + + Assert.IsFalse(result); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public async Task Video_Cancel_CancellationToken_Async_Throws() + { + using var outputFile = new TemporaryFile("out.mp4"); + + var cts = new CancellationTokenSource(); + + var task = FFMpegArguments + .FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args + .WithCustomArgument("-re") + .ForceFormat("lavfi")) + .OutputToFile(outputFile, false, opt => opt + .WithAudioCodec(AudioCodec.Aac) + .WithVideoCodec(VideoCodec.LibX264) + .WithSpeedPreset(Speed.VeryFast)) + .CancellableThrough(cts.Token) + .ProcessAsynchronously(); + + cts.CancelAfter(300); + + await Assert.ThrowsExactlyAsync(() => task); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public void Video_Cancel_CancellationToken_Throws() + { + using var outputFile = new TemporaryFile("out.mp4"); + + var cts = new CancellationTokenSource(); + + var task = FFMpegArguments + .FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args + .WithCustomArgument("-re") + .ForceFormat("lavfi")) + .OutputToFile(outputFile, false, opt => opt + .WithAudioCodec(AudioCodec.Aac) + .WithVideoCodec(VideoCodec.LibX264) + .WithSpeedPreset(Speed.VeryFast)) + .CancellableThrough(cts.Token); + + cts.CancelAfter(300); + + Assert.ThrowsExactly(() => task.ProcessSynchronously()); + } + + [TestMethod] + [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] + public async Task Video_Cancel_CancellationToken_Async_With_Timeout() + { + using var outputFile = new TemporaryFile("out.mp4"); + + var cts = new CancellationTokenSource(); + + var task = FFMpegArguments + .FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args + .WithCustomArgument("-re") + .ForceFormat("lavfi")) + .OutputToFile(outputFile, false, opt => opt + .WithAudioCodec(AudioCodec.Aac) + .WithVideoCodec(VideoCodec.LibX264) + .WithSpeedPreset(Speed.VeryFast)) + .CancellableThrough(cts.Token, 8000) + .ProcessAsynchronously(false); + + cts.CancelAfter(300); + + await task; + + var outputInfo = await FFProbe.AnalyseAsync(outputFile, cancellationToken: TestContext.CancellationToken); + + Assert.IsNotNull(outputInfo); + Assert.AreEqual(320, outputInfo.PrimaryVideoStream!.Width); + Assert.AreEqual(240, outputInfo.PrimaryVideoStream.Height); + Assert.AreEqual("h264", outputInfo.PrimaryVideoStream.CodecName); + Assert.AreEqual("aac", outputInfo.PrimaryAudioStream!.CodecName); + } } diff --git a/FFMpegCore.Test/ffmpeg.config.json b/FFMpegCore.Test/ffmpeg.config.json index b9c9a56..4577f15 100644 --- a/FFMpegCore.Test/ffmpeg.config.json +++ b/FFMpegCore.Test/ffmpeg.config.json @@ -1,3 +1,3 @@ { - "RootDirectory": "" + "BinaryFolder": "" } \ No newline at end of file diff --git a/FFMpegCore.sln b/FFMpegCore.sln index 7ab0929..b99a44e 100644 --- a/FFMpegCore.sln +++ b/FFMpegCore.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.31005.135 +# Visual Studio Version 17 +VisualStudioVersion = 17.7.34003.232 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMpegCore", "FFMpegCore\FFMpegCore.csproj", "{19DE2EC2-9955-4712-8096-C22EF6713E4F}" EndProject @@ -13,6 +13,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMpegCore.Extensions.Syste EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMpegCore.Extensions.SkiaSharp", "FFMpegCore.Extensions.SkiaSharp\FFMpegCore.Extensions.SkiaSharp.csproj", "{5A76F9B7-3681-4551-A9B6-8D3AC5DA1090}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FFMpegCore.Extensions.Downloader", "FFMpegCore.Extensions.Downloader\FFMpegCore.Extensions.Downloader.csproj", "{5FA30158-CAB0-44FD-AD98-C31F5E3D5A56}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution 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}.Release|Any CPU.ActiveCfg = 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 GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/FFMpegCore/Extend/KeyValuePairExtensions.cs b/FFMpegCore/Extend/KeyValuePairExtensions.cs index 4af0904..650015e 100644 --- a/FFMpegCore/Extend/KeyValuePairExtensions.cs +++ b/FFMpegCore/Extend/KeyValuePairExtensions.cs @@ -1,22 +1,21 @@ -namespace FFMpegCore.Extend -{ - internal static class KeyValuePairExtensions - { - /// - /// Concat the two members of a - /// - /// Input object - /// - /// If true encloses the value part between quotes if contains an space character. If false use the - /// value unmodified - /// - /// The formatted string - public static string FormatArgumentPair(this KeyValuePair pair, bool enclose) - { - var key = pair.Key; - var value = enclose ? StringExtensions.EncloseIfContainsSpace(pair.Value) : pair.Value; +namespace FFMpegCore.Extend; - return $"{key}={value}"; - } +internal static class KeyValuePairExtensions +{ + /// + /// Concat the two members of a + /// + /// Input object + /// + /// If true encloses the value part between quotes if contains an space character. If false use the + /// value unmodified + /// + /// The formatted string + public static string FormatArgumentPair(this KeyValuePair pair, bool enclose) + { + var key = pair.Key; + var value = enclose ? StringExtensions.EncloseIfContainsSpace(pair.Value) : pair.Value; + + return $"{key}={value}"; } } diff --git a/FFMpegCore/Extend/PcmAudioSampleWrapper.cs b/FFMpegCore/Extend/PcmAudioSampleWrapper.cs index 7548d63..1a8848a 100644 --- a/FFMpegCore/Extend/PcmAudioSampleWrapper.cs +++ b/FFMpegCore/Extend/PcmAudioSampleWrapper.cs @@ -1,27 +1,26 @@ 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. - //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; + _sample = sample; + } - public PcmAudioSampleWrapper(byte[] sample) - { - _sample = sample; - } + public void Serialize(Stream stream) + { + stream.Write(_sample, 0, _sample.Length); + } - public void Serialize(Stream stream) - { - stream.Write(_sample, 0, _sample.Length); - } - - public async Task SerializeAsync(Stream stream, CancellationToken token) - { - await stream.WriteAsync(_sample, 0, _sample.Length, token).ConfigureAwait(false); - } + public async Task SerializeAsync(Stream stream, CancellationToken token) + { + await stream.WriteAsync(_sample, 0, _sample.Length, token).ConfigureAwait(false); } } diff --git a/FFMpegCore/Extend/StringExtensions.cs b/FFMpegCore/Extend/StringExtensions.cs index 96bcd6c..3a501f4 100644 --- a/FFMpegCore/Extend/StringExtensions.cs +++ b/FFMpegCore/Extend/StringExtensions.cs @@ -1,69 +1,68 @@ using System.Text; -namespace FFMpegCore.Extend +namespace FFMpegCore.Extend; + +internal static class StringExtensions { - internal static class StringExtensions + private static Dictionary CharactersSubstitution { get; } = new() { - private static Dictionary CharactersSubstitution { get; } = new() - { - { '\\', @"\\" }, - { ':', @"\:" }, - { '[', @"\[" }, - { ']', @"\]" }, - { '\'', @"'\\\''" } - }; + { '\\', @"\\" }, + { ':', @"\:" }, + { '[', @"\[" }, + { ']', @"\]" }, + { '\'', @"'\\\''" } + }; - /// - /// Enclose string between quotes if contains an space character - /// - /// The input - /// The enclosed string - public static string EncloseIfContainsSpace(string input) - { - return input.Contains(" ") ? $"'{input}'" : input; - } + /// + /// Enclose string between quotes if contains an space character + /// + /// The input + /// The enclosed string + public static string EncloseIfContainsSpace(string input) + { + return input.Contains(" ") ? $"'{input}'" : input; + } - /// - /// Enclose an string in quotes - /// - /// - /// - public static string EncloseInQuotes(string input) - { - return $"'{input}'"; - } + /// + /// Enclose an string in quotes + /// + /// + /// + public static string EncloseInQuotes(string input) + { + return $"'{input}'"; + } - /// - /// Scape several characters in subtitle path used by FFmpeg - /// - /// - /// This is needed because internally FFmpeg use Libav Filters - /// and the info send to it must be in an specific format - /// - /// - /// Scaped path - public static string ToFFmpegLibavfilterPath(string source) - { - return source.Replace(CharactersSubstitution); - } + /// + /// Scape several characters in subtitle path used by FFmpeg + /// + /// + /// This is needed because internally FFmpeg use Libav Filters + /// and the info send to it must be in an specific format + /// + /// + /// Scaped path + public static string ToFFmpegLibavfilterPath(string source) + { + return source.Replace(CharactersSubstitution); + } - public static string Replace(this string str, Dictionary replaceList) - { - var parsedString = new StringBuilder(); + public static string Replace(this string str, Dictionary replaceList) + { + 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]); - } - else - { - parsedString.Append(l); - } + parsedString.Append(replaceList[l]); + } + else + { + parsedString.Append(l); } - - return parsedString.ToString(); } + + return parsedString.ToString(); } } diff --git a/FFMpegCore/Extend/UriExtensions.cs b/FFMpegCore/Extend/UriExtensions.cs index 9427883..bcb2fcc 100644 --- a/FFMpegCore/Extend/UriExtensions.cs +++ b/FFMpegCore/Extend/UriExtensions.cs @@ -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); } } diff --git a/FFMpegCore/FFMpeg/Arguments/AudibleEncryptionKeyArgument.cs b/FFMpegCore/FFMpeg/Arguments/AudibleEncryptionKeyArgument.cs index 5f7b7dc..cb25665 100644 --- a/FFMpegCore/FFMpeg/Arguments/AudibleEncryptionKeyArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/AudibleEncryptionKeyArgument.cs @@ -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; - - 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}"; + _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}"; } diff --git a/FFMpegCore/FFMpeg/Arguments/AudioBitrateArgument.cs b/FFMpegCore/FFMpeg/Arguments/AudioBitrateArgument.cs index 5e3ed9f..ee5278d 100644 --- a/FFMpegCore/FFMpeg/Arguments/AudioBitrateArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/AudioBitrateArgument.cs @@ -1,19 +1,19 @@ using FFMpegCore.Enums; -namespace FFMpegCore.Arguments -{ - /// - /// Represents parameter of audio codec and it's quality - /// - public class AudioBitrateArgument : IArgument - { - public readonly int Bitrate; - public AudioBitrateArgument(AudioQuality value) : this((int)value) { } - public AudioBitrateArgument(int bitrate) - { - Bitrate = bitrate; - } +namespace FFMpegCore.Arguments; - public string Text => $"-b:a {Bitrate}k"; +/// +/// Represents parameter of audio codec and it's quality +/// +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"; } diff --git a/FFMpegCore/FFMpeg/Arguments/AudioCodecArgument.cs b/FFMpegCore/FFMpeg/Arguments/AudioCodecArgument.cs index e970008..941ab92 100644 --- a/FFMpegCore/FFMpeg/Arguments/AudioCodecArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/AudioCodecArgument.cs @@ -1,30 +1,29 @@ using FFMpegCore.Enums; using FFMpegCore.Exceptions; -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Represents parameter of audio codec and it's quality +/// +public class AudioCodecArgument : IArgument { - /// - /// Represents parameter of audio codec and it's quality - /// - public class AudioCodecArgument : IArgument + public readonly string AudioCodec; + + public AudioCodecArgument(Codec audioCodec) { - public readonly string AudioCodec; - - public AudioCodecArgument(Codec audioCodec) + if (audioCodec.Type != CodecType.Audio) { - if (audioCodec.Type != CodecType.Audio) - { - throw new FFMpegException(FFMpegExceptionType.Operation, $"Codec \"{audioCodec.Name}\" is not an audio codec"); - } - - AudioCodec = audioCodec.Name; + throw new FFMpegException(FFMpegExceptionType.Operation, $"Codec \"{audioCodec.Name}\" is not an audio codec"); } - public AudioCodecArgument(string audioCodec) - { - AudioCodec = audioCodec; - } - - public string Text => $"-c:a {AudioCodec.ToString().ToLowerInvariant()}"; + AudioCodec = audioCodec.Name; } + + public AudioCodecArgument(string audioCodec) + { + AudioCodec = audioCodec; + } + + public string Text => $"-c:a {AudioCodec.ToLowerInvariant()}"; } diff --git a/FFMpegCore/FFMpeg/Arguments/AudioFiltersArgument.cs b/FFMpegCore/FFMpeg/Arguments/AudioFiltersArgument.cs index bbcecb3..cfa2b15 100644 --- a/FFMpegCore/FFMpeg/Arguments/AudioFiltersArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/AudioFiltersArgument.cs @@ -1,71 +1,97 @@ 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(); - - private string GetText() - { - if (!Options.Arguments.Any()) + var arguments = Options.Arguments + .Where(arg => !string.IsNullOrEmpty(arg.Value)) + .Select(arg => { - 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 - .Where(arg => !string.IsNullOrEmpty(arg.Value)) - .Select(arg => - { - var escapedValue = arg.Value.Replace(",", "\\,"); - return string.IsNullOrEmpty(arg.Key) ? escapedValue : $"{arg.Key}={escapedValue}"; - }); - - return $"-af \"{string.Join(", ", arguments)}\""; - } - } - - public interface IAudioFilterArgument - { - string Key { get; } - string Value { get; } - } - - public class AudioFilterOptions - { - public List Arguments { get; } = new(); - - 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, - double gainFactor = 10.0, double targetRms = 0.0, bool channelCoupling = true, - bool enableDcBiasCorrection = false, bool enableAlternativeBoundary = false, - double compressorFactor = 0.0) => WithArgument(new DynamicNormalizerArgument(frameLength, filterWindow, - targetPeak, gainFactor, targetRms, channelCoupling, enableDcBiasCorrection, enableAlternativeBoundary, - compressorFactor)); - public AudioFilterOptions HighPass(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? 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", - int? blocksize = null) => 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") => 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) => WithArgument(new SilenceDetectArgument(noise_type, noise, duration, mono)); - - private AudioFilterOptions WithArgument(IAudioFilterArgument argument) - { - Arguments.Add(argument); - return this; - } + return $"-af \"{string.Join(", ", arguments)}\""; + } +} + +public interface IAudioFilterArgument +{ + string Key { get; } + string Value { get; } +} + +public class AudioFilterOptions +{ + public List Arguments { get; } = new(); + + public AudioFilterOptions Pan(string channelLayout, params string[] outputDefinitions) + { + return WithArgument(new PanArgument(channelLayout, outputDefinitions)); + } + + public AudioFilterOptions Pan(int channels, params string[] outputDefinitions) + { + return WithArgument(new PanArgument(channels, outputDefinitions)); + } + + public AudioFilterOptions DynamicNormalizer(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) + { + return WithArgument(new DynamicNormalizerArgument(frameLength, filterWindow, + targetPeak, gainFactor, targetRms, channelCoupling, enableDcBiasCorrection, enableAlternativeBoundary, + compressorFactor)); + } + + public AudioFilterOptions HighPass(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? blocksize = null) + { + return 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", + int? blocksize = null) + { + 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; } } diff --git a/FFMpegCore/FFMpeg/Arguments/AudioGateArgument.cs b/FFMpegCore/FFMpeg/Arguments/AudioGateArgument.cs index 5b7b405..2e933aa 100644 --- a/FFMpegCore/FFMpeg/Arguments/AudioGateArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/AudioGateArgument.cs @@ -1,98 +1,115 @@ using System.Globalization; -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +public class AudioGateArgument : IAudioFilterArgument { - public class AudioGateArgument : IAudioFilterArgument + private readonly Dictionary _arguments = new(); + + /// + /// Audio Gate. + /// + /// Set input level before filtering. Default is 1. Allowed range is from 0.015625 to 64. + /// + /// 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. + /// + /// + /// 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. + /// + /// If a signal rises above this level the gain reduction is released. Default is 0.125. Allowed range is from 0 to 1. + /// Set a ratio by which the signal is reduced. Default is 2. Allowed range is from 1 to 9000. + /// + /// 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. + /// + /// + /// 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. + /// + /// Set amount of amplification of signal after processing. Default is 1. Allowed range is from 1 to 64. + /// + /// Curve the sharp knee around the threshold to enter gain reduction more softly. Default is 2.828427125. Allowed range is + /// from 1 to 8. + /// + /// Choose if exact signal should be taken for detection or an RMS like one. Default is rms. Can be peak or rms. + /// + /// Choose if the average level between all channels or the louder channel affects the reduction. Default is average. Can be + /// average or maximum. + /// + 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 _arguments = new(); - - /// - /// Audio Gate. - /// - /// Set input level before filtering. Default is 1. Allowed range is from 0.015625 to 64. - /// 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. - /// 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. - /// If a signal rises above this level the gain reduction is released. Default is 0.125. Allowed range is from 0 to 1. - /// Set a ratio by which the signal is reduced. Default is 2. Allowed range is from 1 to 9000. - /// 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. - /// 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. - /// Set amount of amplification of signal after processing. Default is 1. Allowed range is from 1 to 64. - /// Curve the sharp knee around the threshold to enter gain reduction more softly. Default is 2.828427125. Allowed range is from 1 to 8. - /// Choose if exact signal should be taken for detection or an RMS like one. Default is rms. Can be peak or rms. - /// Choose if the average level between all channels or the louder channel affects the reduction. Default is average. Can be average or maximum. - 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) { - if (levelIn is < 0.015625 or > 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); + throw new ArgumentOutOfRangeException(nameof(levelIn), "Level in must be between 0.015625 to 64"); } - 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}")); } diff --git a/FFMpegCore/FFMpeg/Arguments/AudioSamplingRateArgument.cs b/FFMpegCore/FFMpeg/Arguments/AudioSamplingRateArgument.cs index 79e8a39..68ba13b 100644 --- a/FFMpegCore/FFMpeg/Arguments/AudioSamplingRateArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/AudioSamplingRateArgument.cs @@ -1,16 +1,16 @@ -namespace FFMpegCore.Arguments -{ - /// - /// Audio sampling rate argument. Defaults to 48000 (Hz) - /// - public class AudioSamplingRateArgument : IArgument - { - public readonly int SamplingRate; - public AudioSamplingRateArgument(int samplingRate = 48000) - { - SamplingRate = samplingRate; - } +namespace FFMpegCore.Arguments; - public string Text => $"-ar {SamplingRate}"; +/// +/// Audio sampling rate argument. Defaults to 48000 (Hz) +/// +public class AudioSamplingRateArgument : IArgument +{ + public readonly int SamplingRate; + + public AudioSamplingRateArgument(int samplingRate = 48000) + { + SamplingRate = samplingRate; } + + public string Text => $"-ar {SamplingRate}"; } diff --git a/FFMpegCore/FFMpeg/Arguments/BitStreamFilterArgument.cs b/FFMpegCore/FFMpeg/Arguments/BitStreamFilterArgument.cs index e5a4b35..9714ca6 100644 --- a/FFMpegCore/FFMpeg/Arguments/BitStreamFilterArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/BitStreamFilterArgument.cs @@ -1,26 +1,25 @@ using FFMpegCore.Enums; -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Represents parameter of bitstream filter +/// +public class BitStreamFilterArgument : IArgument { - /// - /// Represents parameter of bitstream filter - /// - public class BitStreamFilterArgument : IArgument + public readonly Channel Channel; + public readonly Filter Filter; + + public BitStreamFilterArgument(Channel channel, Filter filter) { - public readonly Channel Channel; - public readonly 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 - }; + 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 + }; } diff --git a/FFMpegCore/FFMpeg/Arguments/BlackDetectArgument.cs b/FFMpegCore/FFMpeg/Arguments/BlackDetectArgument.cs index ee088be..700d11e 100644 --- a/FFMpegCore/FFMpeg/Arguments/BlackDetectArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/BlackDetectArgument.cs @@ -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"; - - 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}"; - } + Value = $"d={minimumDuration}:pic_th={pictureBlackRatioThreshold}:pix_th={pixelBlackThreshold}"; } + + public string Key => "blackdetect"; + + public string Value { get; } } diff --git a/FFMpegCore/FFMpeg/Arguments/BlackFrameArgument.cs b/FFMpegCore/FFMpeg/Arguments/BlackFrameArgument.cs index 9d5bad6..29678b7 100644 --- a/FFMpegCore/FFMpeg/Arguments/BlackFrameArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/BlackFrameArgument.cs @@ -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"; - - public string Value { get; } - - public BlackFrameArgument(int amount = 98, int threshold = 32) - { - Value = $"amount={amount}:threshold={threshold}"; - } + Value = $"amount={amount}:threshold={threshold}"; } + + public string Key => "blackframe"; + + public string Value { get; } } diff --git a/FFMpegCore/FFMpeg/Arguments/ConcatArgument.cs b/FFMpegCore/FFMpeg/Arguments/ConcatArgument.cs index 209bcb0..9ee2e22 100644 --- a/FFMpegCore/FFMpeg/Arguments/ConcatArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/ConcatArgument.cs @@ -1,22 +1,26 @@ -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Represents parameter of concat argument +/// Used for creating video from multiple images or videos +/// +public class ConcatArgument : IInputArgument { + public readonly IEnumerable Values; - /// - /// Represents parameter of concat argument - /// Used for creating video from multiple images or videos - /// - public class ConcatArgument : IInputArgument + public ConcatArgument(IEnumerable values) { - public readonly IEnumerable Values; - public ConcatArgument(IEnumerable 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)}\""; + Values = values; } + + public void Pre() { } + + public Task During(CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + public void Post() { } + + public string Text => $"-i \"concat:{string.Join(@"|", Values)}\""; } diff --git a/FFMpegCore/FFMpeg/Arguments/ConstantRateFactorArgument.cs b/FFMpegCore/FFMpeg/Arguments/ConstantRateFactorArgument.cs index 2736f60..8b1d4d5 100644 --- a/FFMpegCore/FFMpeg/Arguments/ConstantRateFactorArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/ConstantRateFactorArgument.cs @@ -1,22 +1,21 @@ -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Constant Rate Factor (CRF) argument +/// +public class ConstantRateFactorArgument : IArgument { - /// - /// Constant Rate Factor (CRF) argument - /// - public class ConstantRateFactorArgument : IArgument + public readonly int Crf; + + public ConstantRateFactorArgument(int crf) { - public readonly int Crf; - - public ConstantRateFactorArgument(int crf) + if (crf < 0 || crf > 63) { - if (crf < 0 || crf > 63) - { - throw new ArgumentException("Argument is outside range (0 - 63)", nameof(crf)); - } - - Crf = crf; + throw new ArgumentException("Argument is outside range (0 - 63)", nameof(crf)); } - public string Text => $"-crf {Crf}"; + Crf = crf; } + + public string Text => $"-crf {Crf}"; } diff --git a/FFMpegCore/FFMpeg/Arguments/CopyArgument.cs b/FFMpegCore/FFMpeg/Arguments/CopyArgument.cs index eeac4c8..3fd6bdb 100644 --- a/FFMpegCore/FFMpeg/Arguments/CopyArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/CopyArgument.cs @@ -1,23 +1,23 @@ using FFMpegCore.Enums; -namespace FFMpegCore.Arguments -{ - /// - /// Represents parameter of copy parameter - /// Defines if channel (audio, video or both) should be copied to output file - /// - public class CopyArgument : IArgument - { - public readonly Channel Channel; - public CopyArgument(Channel channel = Channel.Both) - { - Channel = channel; - } +namespace FFMpegCore.Arguments; - public string Text => Channel switch - { - Channel.Both => "-c:a copy -c:v copy", - _ => $"-c{Channel.StreamType()} copy" - }; +/// +/// Represents parameter of copy parameter +/// Defines if channel (audio, video or both) should be copied to output file +/// +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" + }; } diff --git a/FFMpegCore/FFMpeg/Arguments/CopyCodecArgument.cs b/FFMpegCore/FFMpeg/Arguments/CopyCodecArgument.cs index 8ea3484..9e18008 100644 --- a/FFMpegCore/FFMpeg/Arguments/CopyCodecArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/CopyCodecArgument.cs @@ -1,10 +1,9 @@ -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Represents a copy codec parameter +/// +public class CopyCodecArgument : IArgument { - /// - /// Represents a copy codec parameter - /// - public class CopyCodecArgument : IArgument - { - public string Text => $"-codec copy"; - } + public string Text => "-codec copy"; } diff --git a/FFMpegCore/FFMpeg/Arguments/CropArgument.cs b/FFMpegCore/FFMpeg/Arguments/CropArgument.cs index 4fe10d6..df14252 100644 --- a/FFMpegCore/FFMpeg/Arguments/CropArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/CropArgument.cs @@ -1,22 +1,21 @@ 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; - public readonly int Top; - public readonly int 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}"; + 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}"; } diff --git a/FFMpegCore/FFMpeg/Arguments/CustomArgument.cs b/FFMpegCore/FFMpeg/Arguments/CustomArgument.cs index c6dc85d..87f2e71 100644 --- a/FFMpegCore/FFMpeg/Arguments/CustomArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/CustomArgument.cs @@ -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; - - public CustomArgument(string argument) - { - Argument = argument; - } - - public string Text => Argument ?? string.Empty; + Argument = argument; } + + public string Text => Argument ?? string.Empty; } diff --git a/FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs b/FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs index 806afd0..24d346e 100644 --- a/FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs @@ -1,31 +1,44 @@ -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Represents parameter of concat argument +/// Used for creating video from multiple images or videos +/// +public class DemuxConcatArgument : IInputArgument { - /// - /// Represents parameter of concat argument - /// Used for creating video from multiple images or videos - /// - public class DemuxConcatArgument : IInputArgument + private readonly string _tempFileName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, $"concat_{Guid.NewGuid()}.txt"); + public readonly IEnumerable Values; + + public DemuxConcatArgument(IEnumerable values) { - public readonly IEnumerable Values; - public DemuxConcatArgument(IEnumerable values) - { - Values = values.Select(value => $"file '{Escape(value)}'"); - } + Values = values.Select(value => $"file '{Escape(value)}'"); + } - /// - /// Thanks slhck - /// https://superuser.com/a/787651/1089628 - /// - /// - /// - private string Escape(string value) => value.Replace("'", @"'\''"); + public void Pre() + { + File.WriteAllLines(_tempFileName, Values); + } - 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 Task During(CancellationToken cancellationToken = default) => Task.CompletedTask; - public void Post() => File.Delete(_tempFileName); + public void Post() + { + File.Delete(_tempFileName); + } - public string Text => $"-f concat -safe 0 -i \"{_tempFileName}\""; + public string Text => $"-f concat -safe 0 -i \"{_tempFileName}\""; + + /// + /// Thanks slhck + /// https://superuser.com/a/787651/1089628 + /// + /// + /// + private string Escape(string value) + { + return value.Replace("'", @"'\''"); } } diff --git a/FFMpegCore/FFMpeg/Arguments/DisableChannelArgument.cs b/FFMpegCore/FFMpeg/Arguments/DisableChannelArgument.cs index b6dd918..c6b89ea 100644 --- a/FFMpegCore/FFMpeg/Arguments/DisableChannelArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/DisableChannelArgument.cs @@ -1,30 +1,29 @@ using FFMpegCore.Enums; using FFMpegCore.Exceptions; -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Represents cpu speed parameter +/// +public class DisableChannelArgument : IArgument { - /// - /// Represents cpu speed parameter - /// - public class DisableChannelArgument : IArgument + public readonly Channel Channel; + + public DisableChannelArgument(Channel channel) { - public readonly Channel Channel; - - public DisableChannelArgument(Channel channel) + if (channel == Channel.Both) { - if (channel == Channel.Both) - { - throw new FFMpegException(FFMpegExceptionType.Conversion, "Cannot disable both channels"); - } - - Channel = channel; + throw new FFMpegException(FFMpegExceptionType.Conversion, "Cannot disable both channels"); } - public string Text => Channel switch - { - Channel.Video => "-vn", - Channel.Audio => "-an", - _ => string.Empty - }; + Channel = channel; } + + public string Text => Channel switch + { + Channel.Video => "-vn", + Channel.Audio => "-an", + _ => string.Empty + }; } diff --git a/FFMpegCore/FFMpeg/Arguments/DrawTextArgument.cs b/FFMpegCore/FFMpeg/Arguments/DrawTextArgument.cs index 11f240b..1b6219a 100644 --- a/FFMpegCore/FFMpeg/Arguments/DrawTextArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/DrawTextArgument.cs @@ -1,59 +1,59 @@ -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Drawtext video filter argument +/// +public class DrawTextArgument : IVideoFilterArgument { - /// - /// Drawtext video filter argument - /// - public class DrawTextArgument : IVideoFilterArgument + public readonly DrawTextOptions Options; + + public DrawTextArgument(DrawTextOptions options) { - public readonly DrawTextOptions Options; - - public DrawTextArgument(DrawTextOptions options) - { - Options = options; - } - - public string Key { get; } = "drawtext"; - public string Value => Options.TextInternal; + Options = options; } - 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; - public readonly string Font; - public readonly List<(string key, string value)> Parameters; + Text = text; + Font = font; + Parameters = parameters.ToList(); + } - public static DrawTextOptions Create(string text, string font) - { - 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)); - 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) - { - return $"{pair.key}={EncloseIfContainsSpace(pair.value)}"; - } + public static DrawTextOptions Create(string text, string font, params (string key, string value)[] parameters) + { + return new DrawTextOptions(text, font, parameters); + } - private static string EncloseIfContainsSpace(string input) - { - return input.Contains(" ") ? $"'{input}'" : input; - } + private static string FormatArgumentPair((string key, string value) pair) + { + return $"{pair.key}={EncloseIfContainsSpace(pair.value)}"; + } - private DrawTextOptions(string text, string font, IEnumerable<(string, string)> parameters) - { - Text = text; - Font = font; - Parameters = parameters.ToList(); - } + private static string EncloseIfContainsSpace(string input) + { + return input.Contains(" ") ? $"'{input}'" : input; + } - public DrawTextOptions WithParameter(string key, string value) - { - Parameters.Add((key, value)); - return this; - } + public DrawTextOptions WithParameter(string key, string value) + { + Parameters.Add((key, value)); + return this; } } diff --git a/FFMpegCore/FFMpeg/Arguments/DurationArgument.cs b/FFMpegCore/FFMpeg/Arguments/DurationArgument.cs index f1da817..e122860 100644 --- a/FFMpegCore/FFMpeg/Arguments/DurationArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/DurationArgument.cs @@ -1,16 +1,16 @@ -namespace FFMpegCore.Arguments -{ - /// - /// Represents duration parameter - /// - public class DurationArgument : IArgument - { - public readonly TimeSpan? Duration; - public DurationArgument(TimeSpan? duration) - { - Duration = duration; - } +namespace FFMpegCore.Arguments; - public string Text => !Duration.HasValue ? string.Empty : $"-t {Duration.Value}"; +/// +/// Represents duration parameter +/// +public class DurationArgument : IArgument +{ + public readonly TimeSpan? Duration; + + public DurationArgument(TimeSpan? duration) + { + Duration = duration; } + + public string Text => !Duration.HasValue ? string.Empty : $"-t {Duration.Value}"; } diff --git a/FFMpegCore/FFMpeg/Arguments/DynamicNormalizerArgument.cs b/FFMpegCore/FFMpeg/Arguments/DynamicNormalizerArgument.cs index 518afee..5537cb4 100644 --- a/FFMpegCore/FFMpeg/Arguments/DynamicNormalizerArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/DynamicNormalizerArgument.cs @@ -1,73 +1,73 @@ using System.Globalization; -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +public class DynamicNormalizerArgument : IAudioFilterArgument { - public class DynamicNormalizerArgument : IAudioFilterArgument + private readonly Dictionary _arguments = new(); + + /// + /// Dynamic Audio Normalizer. + /// + /// Set the frame length in milliseconds. Must be between 10 to 8000. The default value is 500 + /// Set the Gaussian filter window size. In range from 3 to 301, must be odd number. The default value is 31 + /// Set the target peak value. The default value is 0.95 + /// Set the maximum gain factor. In range from 1.0 to 100.0. Default is 10.0. + /// Set the target RMS. In range from 0.0 to 1.0. Default to 0.0 (disabled) + /// Enable channels coupling. By default is enabled. + /// Enable DC bias correction. By default is disabled. + /// Enable alternative boundary mode. By default is disabled. + /// Set the compress factor. In range from 0.0 to 30.0. Default is 0.0 (disabled). + 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 _arguments = new(); - - /// - /// Dynamic Audio Normalizer. - /// - /// Set the frame length in milliseconds. Must be between 10 to 8000. The default value is 500 - /// Set the Gaussian filter window size. In range from 3 to 301, must be odd number. The default value is 31 - /// Set the target peak value. The default value is 0.95 - /// Set the maximum gain factor. In range from 1.0 to 100.0. Default is 10.0. - /// Set the target RMS. In range from 0.0 to 1.0. Default to 0.0 (disabled) - /// Enable channels coupling. By default is enabled. - /// Enable DC bias correction. By default is disabled. - /// Enable alternative boundary mode. By default is disabled. - /// Set the compress factor. In range from 0.0 to 30.0. Default is 0.0 (disabled). - 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) { - if (frameLength < 10 || frameLength > 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)); + throw new ArgumentOutOfRangeException(nameof(frameLength), "Frame length must be between 10 to 8000"); } - 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}")); } diff --git a/FFMpegCore/FFMpeg/Arguments/EndSeekArgument.cs b/FFMpegCore/FFMpeg/Arguments/EndSeekArgument.cs index e4e8f5d..29b65da 100644 --- a/FFMpegCore/FFMpeg/Arguments/EndSeekArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/EndSeekArgument.cs @@ -1,19 +1,18 @@ using FFMpegCore.Extend; -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Represents seek parameter +/// +public class EndSeekArgument : IArgument { - /// - /// Represents seek parameter - /// - public class EndSeekArgument : IArgument + public readonly TimeSpan? SeekTo; + + public EndSeekArgument(TimeSpan? seekTo) { - public readonly TimeSpan? SeekTo; - - public EndSeekArgument(TimeSpan? seekTo) - { - SeekTo = seekTo; - } - - public string Text => SeekTo.HasValue ? $"-to {SeekTo.Value.ToLongString()}" : string.Empty; + SeekTo = seekTo; } + + public string Text => SeekTo.HasValue ? $"-to {SeekTo.Value.ToLongString()}" : string.Empty; } diff --git a/FFMpegCore/FFMpeg/Arguments/FaststartArgument.cs b/FFMpegCore/FFMpeg/Arguments/FaststartArgument.cs index 185d67e..2d4eb55 100644 --- a/FFMpegCore/FFMpeg/Arguments/FaststartArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/FaststartArgument.cs @@ -1,10 +1,9 @@ -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Faststart argument - for moving moov atom to the start of file +/// +public class FaststartArgument : IArgument { - /// - /// Faststart argument - for moving moov atom to the start of file - /// - public class FaststartArgument : IArgument - { - public string Text => "-movflags faststart"; - } + public string Text => "-movflags faststart"; } diff --git a/FFMpegCore/FFMpeg/Arguments/ForceFormatArgument.cs b/FFMpegCore/FFMpeg/Arguments/ForceFormatArgument.cs index 9524698..7cad6ac 100644 --- a/FFMpegCore/FFMpeg/Arguments/ForceFormatArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/ForceFormatArgument.cs @@ -1,23 +1,23 @@ using FFMpegCore.Enums; -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Represents force format parameter +/// +public class ForceFormatArgument : IArgument { - /// - /// Represents force format parameter - /// - public class ForceFormatArgument : IArgument + private readonly string _format; + + public ForceFormatArgument(string format) { - private readonly string _format; - public ForceFormatArgument(string format) - { - _format = format; - } - - public ForceFormatArgument(ContainerFormat format) - { - _format = format.Name; - } - - public string Text => $"-f {_format}"; + _format = format; } + + public ForceFormatArgument(ContainerFormat format) + { + _format = format.Name; + } + + public string Text => $"-f {_format}"; } diff --git a/FFMpegCore/FFMpeg/Arguments/ForcePixelFormat.cs b/FFMpegCore/FFMpeg/Arguments/ForcePixelFormat.cs index 8402552..023dee8 100644 --- a/FFMpegCore/FFMpeg/Arguments/ForcePixelFormat.cs +++ b/FFMpegCore/FFMpeg/Arguments/ForcePixelFormat.cs @@ -1,17 +1,15 @@ 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; } - public string Text => $"-pix_fmt {PixelFormat}"; - - public ForcePixelFormat(string format) - { - PixelFormat = format; - } - - public ForcePixelFormat(PixelFormat format) : this(format.Name) { } + PixelFormat = format; } + + public ForcePixelFormat(PixelFormat format) : this(format.Name) { } + public string PixelFormat { get; } + public string Text => $"-pix_fmt {PixelFormat}"; } diff --git a/FFMpegCore/FFMpeg/Arguments/FrameOutputCountArgument.cs b/FFMpegCore/FFMpeg/Arguments/FrameOutputCountArgument.cs index 08bc56b..ff812f2 100644 --- a/FFMpegCore/FFMpeg/Arguments/FrameOutputCountArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/FrameOutputCountArgument.cs @@ -1,16 +1,16 @@ -namespace FFMpegCore.Arguments -{ - /// - /// Represents frame output count parameter - /// - public class FrameOutputCountArgument : IArgument - { - public readonly int Frames; - public FrameOutputCountArgument(int frames) - { - Frames = frames; - } +namespace FFMpegCore.Arguments; - public string Text => $"-vframes {Frames}"; +/// +/// Represents frame output count parameter +/// +public class FrameOutputCountArgument : IArgument +{ + public readonly int Frames; + + public FrameOutputCountArgument(int frames) + { + Frames = frames; } + + public string Text => $"-vframes {Frames}"; } diff --git a/FFMpegCore/FFMpeg/Arguments/FrameRateArgument.cs b/FFMpegCore/FFMpeg/Arguments/FrameRateArgument.cs index 7c921af..7bdacad 100644 --- a/FFMpegCore/FFMpeg/Arguments/FrameRateArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/FrameRateArgument.cs @@ -1,17 +1,18 @@ -namespace FFMpegCore.Arguments +using System.Globalization; + +namespace FFMpegCore.Arguments; + +/// +/// Represents frame rate parameter +/// +public class FrameRateArgument : IArgument { - /// - /// Represents frame rate parameter - /// - public class FrameRateArgument : IArgument + public readonly double Framerate; + + public FrameRateArgument(double framerate) { - public readonly double Framerate; - - public FrameRateArgument(double framerate) - { - Framerate = framerate; - } - - public string Text => $"-r {Framerate.ToString(System.Globalization.CultureInfo.InvariantCulture)}"; + Framerate = framerate; } + + public string Text => $"-r {Framerate.ToString(CultureInfo.InvariantCulture)}"; } diff --git a/FFMpegCore/FFMpeg/Arguments/GifPaletteArgument.cs b/FFMpegCore/FFMpeg/Arguments/GifPaletteArgument.cs index ac67fcd..7e4db86 100644 --- a/FFMpegCore/FFMpeg/Arguments/GifPaletteArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/GifPaletteArgument.cs @@ -1,24 +1,23 @@ 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; - - private readonly int _fps; - - 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\""; + _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\""; } diff --git a/FFMpegCore/FFMpeg/Arguments/HardwareAccelerationArgument.cs b/FFMpegCore/FFMpeg/Arguments/HardwareAccelerationArgument.cs index d447879..e7d1cb9 100644 --- a/FFMpegCore/FFMpeg/Arguments/HardwareAccelerationArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/HardwareAccelerationArgument.cs @@ -1,16 +1,15 @@ 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; } - - public HardwareAccelerationArgument(HardwareAccelerationDevice hardwareAccelerationDevice) - { - HardwareAccelerationDevice = hardwareAccelerationDevice; - } - - public string Text => $"-hwaccel {HardwareAccelerationDevice.ToString().ToLower()}"; + HardwareAccelerationDevice = hardwareAccelerationDevice; } + + public HardwareAccelerationDevice HardwareAccelerationDevice { get; } + + public string Text => $"-hwaccel {HardwareAccelerationDevice.ToString().ToLower()}"; } diff --git a/FFMpegCore/FFMpeg/Arguments/HighPassFilterArgument.cs b/FFMpegCore/FFMpeg/Arguments/HighPassFilterArgument.cs index cf7ccfd..8675b2c 100644 --- a/FFMpegCore/FFMpeg/Arguments/HighPassFilterArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/HighPassFilterArgument.cs @@ -1,78 +1,112 @@ using System.Globalization; -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +public class HighPassFilterArgument : IAudioFilterArgument { - public class HighPassFilterArgument : IAudioFilterArgument + private readonly Dictionary _arguments = new(); + + private readonly List _precision = new() { - private readonly Dictionary _arguments = new(); - private readonly List _widthTypes = new() { "h", "q", "o", "s", "k" }; - private readonly List _transformTypes = new() { "di", "dii", "tdi", "tdii", "latt", "svf", "zdf" }; - private readonly List _precision = new() { "auto", "s16", "s32", "f32", "f64" }; - /// - /// HighPass Filter. - /// - /// Set frequency in Hz. Default is 3000. - /// Set number of poles. Default is 2. - /// Set method to specify band-width of filter, possible values are: h, q, o, s, k - /// 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. - /// How much to use filtered signal in output. Default is 1. Range is between 0 and 1. - /// Specify which channels to filter, by default all available are filtered. - /// Normalize biquad coefficients, by default is disabled. Enabling it will normalize magnitude response at DC to 0dB. - /// Set transform type of IIR filter, possible values are: di, dii, tdi, tdii, latt, svf, zdf - /// Set precison of filtering, possible values are: auto, s16, s32, f32, f64. - /// 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. - 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) + "auto", + "s16", + "s32", + "f32", + "f64" + }; + + private readonly List _transformTypes = new() + { + "di", + "dii", + "tdi", + "tdii", + "latt", + "svf", + "zdf" + }; + + private readonly List _widthTypes = new() + { + "h", + "q", + "o", + "s", + "k" + }; + + /// + /// HighPass Filter. + /// + /// Set frequency in Hz. Default is 3000. + /// Set number of poles. Default is 2. + /// Set method to specify band-width of filter, possible values are: h, q, o, s, k + /// + /// 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. + /// + /// How much to use filtered signal in output. Default is 1. Range is between 0 and 1. + /// Specify which channels to filter, by default all available are filtered. + /// Normalize biquad coefficients, by default is disabled. Enabling it will normalize magnitude response at DC to 0dB. + /// Set transform type of IIR filter, possible values are: di, dii, tdi, tdii, latt, svf, zdf + /// Set precison of filtering, possible values are: auto, s16, s32, f32, f64. + /// + /// 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. + /// + 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"); - } - - 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()); - } + throw new ArgumentOutOfRangeException(nameof(frequency), "Frequency must be a positive number"); } - 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}")); } diff --git a/FFMpegCore/FFMpeg/Arguments/IArgument.cs b/FFMpegCore/FFMpeg/Arguments/IArgument.cs index 2416b5d..ab04a69 100644 --- a/FFMpegCore/FFMpeg/Arguments/IArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/IArgument.cs @@ -1,10 +1,9 @@ -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +public interface IArgument { - public interface IArgument - { - /// - /// The textual representation of the argument - /// - string Text { get; } - } + /// + /// The textual representation of the argument + /// + string Text { get; } } diff --git a/FFMpegCore/FFMpeg/Arguments/ID3V2VersionArgument.cs b/FFMpegCore/FFMpeg/Arguments/ID3V2VersionArgument.cs index e18d93b..49e804d 100644 --- a/FFMpegCore/FFMpeg/Arguments/ID3V2VersionArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/ID3V2VersionArgument.cs @@ -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; - - public ID3V2VersionArgument(int version) - { - _version = version; - } - - public string Text => $"-id3v2_version {_version}"; + _version = version; } + + public string Text => $"-id3v2_version {_version}"; } diff --git a/FFMpegCore/FFMpeg/Arguments/IDynamicArgument.cs b/FFMpegCore/FFMpeg/Arguments/IDynamicArgument.cs index d44d18d..6736f63 100644 --- a/FFMpegCore/FFMpeg/Arguments/IDynamicArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/IDynamicArgument.cs @@ -1,13 +1,12 @@ -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +public interface IDynamicArgument { - public interface IDynamicArgument - { - /// - /// Same as , but this receives the arguments generated before as parameter - /// - /// - /// - //public string GetText(StringBuilder context); - public string GetText(IEnumerable context); - } + /// + /// Same as , but this receives the arguments generated before as parameter + /// + /// + /// + //public string GetText(StringBuilder context); + string GetText(IEnumerable context); } diff --git a/FFMpegCore/FFMpeg/Arguments/IInputArgument.cs b/FFMpegCore/FFMpeg/Arguments/IInputArgument.cs index c4a9e3c..36e7704 100644 --- a/FFMpegCore/FFMpeg/Arguments/IInputArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/IInputArgument.cs @@ -1,6 +1,5 @@ -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +public interface IInputArgument : IInputOutputArgument { - public interface IInputArgument : IInputOutputArgument - { - } } diff --git a/FFMpegCore/FFMpeg/Arguments/IInputOutputArgument.cs b/FFMpegCore/FFMpeg/Arguments/IInputOutputArgument.cs index b925b58..b8c3487 100644 --- a/FFMpegCore/FFMpeg/Arguments/IInputOutputArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/IInputOutputArgument.cs @@ -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 Post(); - } + void Pre(); + Task During(CancellationToken cancellationToken = default); + void Post(); } diff --git a/FFMpegCore/FFMpeg/Arguments/IOutputArgument.cs b/FFMpegCore/FFMpeg/Arguments/IOutputArgument.cs index 590c819..70289b8 100644 --- a/FFMpegCore/FFMpeg/Arguments/IOutputArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/IOutputArgument.cs @@ -1,6 +1,5 @@ -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +public interface IOutputArgument : IInputOutputArgument { - public interface IOutputArgument : IInputOutputArgument - { - } } diff --git a/FFMpegCore/FFMpeg/Arguments/InputArgument.cs b/FFMpegCore/FFMpeg/Arguments/InputArgument.cs index 4d6f123..f3e08d3 100644 --- a/FFMpegCore/FFMpeg/Arguments/InputArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/InputArgument.cs @@ -1,32 +1,35 @@ -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Represents input parameter +/// +public class InputArgument : IInputArgument { - /// - /// Represents input parameter - /// - public class InputArgument : IInputArgument + public readonly string FilePath; + public readonly bool VerifyExists; + + public InputArgument(bool verifyExists, string filePaths) { - public readonly bool VerifyExists; - public readonly string FilePath; - - 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}\""; + 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) + { + return Task.CompletedTask; + } + + public void Post() { } + + public string Text => $"-i \"{FilePath}\""; } diff --git a/FFMpegCore/FFMpeg/Arguments/InputDeviceArgument.cs b/FFMpegCore/FFMpeg/Arguments/InputDeviceArgument.cs index c6fecd2..1c7b93e 100644 --- a/FFMpegCore/FFMpeg/Arguments/InputDeviceArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/InputDeviceArgument.cs @@ -1,23 +1,25 @@ -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Represents an input device parameter +/// +public class InputDeviceArgument : IInputArgument { - /// - /// Represents an input device parameter - /// - public class InputDeviceArgument : IInputArgument + private readonly string Device; + + public InputDeviceArgument(string device) { - private readonly string 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}"; + Device = device; } + + public Task During(CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + public void Pre() { } + + public void Post() { } + + public string Text => $"-i {Device}"; } diff --git a/FFMpegCore/FFMpeg/Arguments/InputPipeArgument.cs b/FFMpegCore/FFMpeg/Arguments/InputPipeArgument.cs index d28133f..1c21a3d 100644 --- a/FFMpegCore/FFMpeg/Arguments/InputPipeArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/InputPipeArgument.cs @@ -1,31 +1,30 @@ using System.IO.Pipes; using FFMpegCore.Pipes; -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Represents input parameter for a named pipe +/// +public class InputPipeArgument : PipeArgument, IInputArgument { - /// - /// Represents input parameter for a named pipe - /// - public class InputPipeArgument : PipeArgument, IInputArgument + public readonly IPipeSource Writer; + + public InputPipeArgument(IPipeSource writer) : base(PipeDirection.Out) { - 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}\""; - - 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); - } + await Writer.WriteAsync(Pipe, token).ConfigureAwait(false); } } diff --git a/FFMpegCore/FFMpeg/Arguments/LoopArgument.cs b/FFMpegCore/FFMpeg/Arguments/LoopArgument.cs index 26adc3e..fd4ec04 100644 --- a/FFMpegCore/FFMpeg/Arguments/LoopArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/LoopArgument.cs @@ -1,16 +1,16 @@ -namespace FFMpegCore.Arguments -{ - /// - /// Represents loop parameter - /// - public class LoopArgument : IArgument - { - public readonly int Times; - public LoopArgument(int times) - { - Times = times; - } +namespace FFMpegCore.Arguments; - public string Text => $"-loop {Times}"; +/// +/// Represents loop parameter +/// +public class LoopArgument : IArgument +{ + public readonly int Times; + + public LoopArgument(int times) + { + Times = times; } + + public string Text => $"-loop {Times}"; } diff --git a/FFMpegCore/FFMpeg/Arguments/LowPassFilterArgument.cs b/FFMpegCore/FFMpeg/Arguments/LowPassFilterArgument.cs index add0209..8240a1a 100644 --- a/FFMpegCore/FFMpeg/Arguments/LowPassFilterArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/LowPassFilterArgument.cs @@ -1,78 +1,112 @@ using System.Globalization; -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +public class LowPassFilterArgument : IAudioFilterArgument { - public class LowPassFilterArgument : IAudioFilterArgument + private readonly Dictionary _arguments = new(); + + private readonly List _precision = new() { - private readonly Dictionary _arguments = new(); - private readonly List _widthTypes = new() { "h", "q", "o", "s", "k" }; - private readonly List _transformTypes = new() { "di", "dii", "tdi", "tdii", "latt", "svf", "zdf" }; - private readonly List _precision = new() { "auto", "s16", "s32", "f32", "f64" }; - /// - /// LowPass Filter. - /// - /// Set frequency in Hz. Default is 3000. - /// Set number of poles. Default is 2. - /// Set method to specify band-width of filter, possible values are: h, q, o, s, k - /// 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. - /// How much to use filtered signal in output. Default is 1. Range is between 0 and 1. - /// Specify which channels to filter, by default all available are filtered. - /// Normalize biquad coefficients, by default is disabled. Enabling it will normalize magnitude response at DC to 0dB. - /// Set transform type of IIR filter, possible values are: di, dii, tdi, tdii, latt, svf, zdf - /// Set precison of filtering, possible values are: auto, s16, s32, f32, f64. - /// 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. - 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) + "auto", + "s16", + "s32", + "f32", + "f64" + }; + + private readonly List _transformTypes = new() + { + "di", + "dii", + "tdi", + "tdii", + "latt", + "svf", + "zdf" + }; + + private readonly List _widthTypes = new() + { + "h", + "q", + "o", + "s", + "k" + }; + + /// + /// LowPass Filter. + /// + /// Set frequency in Hz. Default is 3000. + /// Set number of poles. Default is 2. + /// Set method to specify band-width of filter, possible values are: h, q, o, s, k + /// + /// 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. + /// + /// How much to use filtered signal in output. Default is 1. Range is between 0 and 1. + /// Specify which channels to filter, by default all available are filtered. + /// Normalize biquad coefficients, by default is disabled. Enabling it will normalize magnitude response at DC to 0dB. + /// Set transform type of IIR filter, possible values are: di, dii, tdi, tdii, latt, svf, zdf + /// Set precison of filtering, possible values are: auto, s16, s32, f32, f64. + /// + /// 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. + /// + 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"); - } - - 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()); - } + throw new ArgumentOutOfRangeException(nameof(frequency), "Frequency must be a positive number"); } - 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}")); } diff --git a/FFMpegCore/FFMpeg/Arguments/MapMetadataArgument.cs b/FFMpegCore/FFMpeg/Arguments/MapMetadataArgument.cs index 218de1b..2f7ecd4 100644 --- a/FFMpegCore/FFMpeg/Arguments/MapMetadataArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/MapMetadataArgument.cs @@ -1,53 +1,52 @@ -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +public class MapMetadataArgument : IInputArgument, IDynamicArgument { - public class MapMetadataArgument : IInputArgument, IDynamicArgument + private readonly int? _inputIndex; + + /// + /// Null means it takes the last input used before this argument + /// + /// + public MapMetadataArgument(int? inputIndex = null) { - private readonly int? _inputIndex; + _inputIndex = inputIndex; + } - public string Text => GetText(null); + public string GetText(IEnumerable? arguments) + { + arguments ??= Enumerable.Empty(); - /// - /// Null means it takes the last input used before this argument - /// - /// - public MapMetadataArgument(int? inputIndex = null) + var index = 0; + if (_inputIndex is null) { - _inputIndex = inputIndex; + index = arguments + .TakeWhile(x => x != this) + .OfType() + .Count(); + + index = Math.Max(index - 1, 0); + } + else + { + index = _inputIndex.Value; } - public string GetText(IEnumerable? arguments) - { - arguments ??= Enumerable.Empty(); + return $"-map_metadata {index}"; + } - var index = 0; - if (_inputIndex is null) - { - index = arguments - .TakeWhile(x => x != this) - .OfType() - .Count(); + public string Text => GetText(null); - index = Math.Max(index - 1, 0); - } - else - { - index = _inputIndex.Value; - } + public Task During(CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } - return $"-map_metadata {index}"; - } + public void Post() + { + } - public Task During(CancellationToken cancellationToken = default) - { - return Task.CompletedTask; - } - - public void Post() - { - } - - public void Pre() - { - } + public void Pre() + { } } diff --git a/FFMpegCore/FFMpeg/Arguments/MapStreamArgument.cs b/FFMpegCore/FFMpeg/Arguments/MapStreamArgument.cs index 2c1c5fd..2b0aa63 100644 --- a/FFMpegCore/FFMpeg/Arguments/MapStreamArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/MapStreamArgument.cs @@ -1,31 +1,30 @@ using FFMpegCore.Enums; -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Represents choice of stream by the stream specifier +/// +public class MapStreamArgument : IArgument { - /// - /// Represents choice of stream by the stream specifier - /// - public class MapStreamArgument : IArgument + private readonly Channel _channel; + private readonly int _inputFileIndex; + private readonly bool _negativeMap; + private readonly int _streamIndex; + + public MapStreamArgument(int streamIndex, int inputFileIndex, Channel channel = Channel.All, bool negativeMap = false) { - private readonly int _inputFileIndex; - 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) { - if (channel == Channel.Both) - { - // "Both" is not valid in this case and probably means all stream types - channel = Channel.All; - } - - _inputFileIndex = inputFileIndex; - _streamIndex = streamIndex; - _channel = channel; - _negativeMap = negativeMap; + // "Both" is not valid in this case and probably means all stream types + channel = Channel.All; } - 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}"; } diff --git a/FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs b/FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs index 02e3f7a..9cbcb25 100644 --- a/FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs @@ -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; - private readonly string _tempFileName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, $"metadata_{Guid.NewGuid()}.txt"); + _metaDataContent = metaDataContent; + } - public MetaDataArgument(string metaDataContent) - { - _metaDataContent = metaDataContent; - } + public string GetText(IEnumerable? arguments) + { + arguments ??= Enumerable.Empty(); - public string Text => GetText(null); + var index = arguments + .TakeWhile(x => x != this) + .OfType() + .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? arguments) - { - arguments ??= Enumerable.Empty(); + public void Pre() + { + File.WriteAllText(_tempFileName, _metaDataContent); + } - var index = arguments - .TakeWhile(x => x != this) - .OfType() - .Count(); - - return $"-i \"{_tempFileName}\" -map_metadata {index}"; - } + public void Post() + { + File.Delete(_tempFileName); } } diff --git a/FFMpegCore/FFMpeg/Arguments/MultiInputArgument.cs b/FFMpegCore/FFMpeg/Arguments/MultiInputArgument.cs index 288c761..e004b49 100644 --- a/FFMpegCore/FFMpeg/Arguments/MultiInputArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/MultiInputArgument.cs @@ -1,47 +1,50 @@ -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Represents input parameters for multiple files +/// +public class MultiInputArgument : IInputArgument { - /// - /// Represents input parameters for multiple files - /// - public class MultiInputArgument : IInputArgument + public readonly IEnumerable FilePaths; + public readonly bool VerifyExists; + + public MultiInputArgument(bool verifyExists, IEnumerable filePaths) { - public readonly bool VerifyExists; - public readonly IEnumerable FilePaths; + VerifyExists = verifyExists; + FilePaths = filePaths; + } - public MultiInputArgument(bool verifyExists, IEnumerable filePaths) + public MultiInputArgument(IEnumerable filePaths, bool verifyExists) : this(verifyExists, filePaths) { } + + public void Pre() + { + if (VerifyExists) { - VerifyExists = verifyExists; - FilePaths = filePaths; - } - - public MultiInputArgument(IEnumerable filePaths, bool verifyExists) : this(verifyExists, filePaths) { } - - public void Pre() - { - if (VerifyExists) + var missingFiles = new List(); + foreach (var filePath in FilePaths) { - var missingFiles = new List(); - foreach (var filePath in FilePaths) + if (!File.Exists(filePath)) { - if (!File.Exists(filePath)) - { - missingFiles.Add(filePath); - } - } - - if (missingFiles.Any()) - { - throw new FileNotFoundException($"The following input files were not found: {string.Join(", ", missingFiles)}"); + missingFiles.Add(filePath); } } + + 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() { } - - /// - /// Generates a combined input argument text for all file paths - /// - public string Text => string.Join(" ", FilePaths.Select(filePath => $"-i \"{filePath}\"")); } + + public Task During(CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + public void Post() { } + + /// + /// Generates a combined input argument text for all file paths + /// + public string Text => string.Join(" ", FilePaths.Select(filePath => $"-i \"{filePath}\"")); } diff --git a/FFMpegCore/FFMpeg/Arguments/OutputArgument.cs b/FFMpegCore/FFMpeg/Arguments/OutputArgument.cs index d5793aa..250a873 100644 --- a/FFMpegCore/FFMpeg/Arguments/OutputArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/OutputArgument.cs @@ -1,37 +1,41 @@ using FFMpegCore.Exceptions; -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Represents output parameter +/// +public class OutputArgument : IOutputArgument { - /// - /// Represents output parameter - /// - public class OutputArgument : IOutputArgument + public readonly bool Overwrite; + public readonly string Path; + + public OutputArgument(string path, bool overwrite = true) { - public readonly string Path; - public readonly bool 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)}"; + Path = path; + Overwrite = overwrite; } + + 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)}"; } diff --git a/FFMpegCore/FFMpeg/Arguments/OutputPipeArgument.cs b/FFMpegCore/FFMpeg/Arguments/OutputPipeArgument.cs index d21cc04..a555aff 100644 --- a/FFMpegCore/FFMpeg/Arguments/OutputPipeArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/OutputPipeArgument.cs @@ -1,28 +1,27 @@ using System.IO.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"; - - 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); - } + await Reader.ReadAsync(Pipe, token).ConfigureAwait(false); } } diff --git a/FFMpegCore/FFMpeg/Arguments/OutputTeeArgument.cs b/FFMpegCore/FFMpeg/Arguments/OutputTeeArgument.cs index 974b928..4a241ec 100644 --- a/FFMpegCore/FFMpeg/Arguments/OutputTeeArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/OutputTeeArgument.cs @@ -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; - - public OutputTeeArgument(FFMpegMultiOutputOptions options) + if (options.Outputs.Count == 0) { - if (options.Outputs.Count == 0) - { - throw new ArgumentException("Atleast one output must be specified.", nameof(options)); - } - - _options = options; + throw new ArgumentException("Atleast one output must be specified.", nameof(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().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; - 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().Single(); - return $"{optionPrefix}{output.Text.Trim('"')}"; + return bitstreamFilter.Text.Replace("-bsf:", "bsfs/").Replace(' ', '='); } - private static string MapArgument(IArgument argument) - { - 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(' ', '='); - } + return argument.Text.TrimStart('-').Replace(' ', '='); } } diff --git a/FFMpegCore/FFMpeg/Arguments/OutputUrlArgument.cs b/FFMpegCore/FFMpeg/Arguments/OutputUrlArgument.cs index 36107c4..059eca1 100644 --- a/FFMpegCore/FFMpeg/Arguments/OutputUrlArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/OutputUrlArgument.cs @@ -1,24 +1,26 @@ -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Represents outputting to url using supported protocols +/// See http://ffmpeg.org/ffmpeg-protocols.html +/// +public class OutputUrlArgument : IOutputArgument { - /// - /// Represents outputting to url using supported protocols - /// See http://ffmpeg.org/ffmpeg-protocols.html - /// - public class OutputUrlArgument : IOutputArgument + public readonly string Url; + + public OutputUrlArgument(string url) { - public readonly string 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; + Url = url; } + + public void Post() { } + + public Task During(CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + public void Pre() { } + + public string Text => Url; } diff --git a/FFMpegCore/FFMpeg/Arguments/OverwriteArgument.cs b/FFMpegCore/FFMpeg/Arguments/OverwriteArgument.cs index 3a633af..8bb2dbc 100644 --- a/FFMpegCore/FFMpeg/Arguments/OverwriteArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/OverwriteArgument.cs @@ -1,11 +1,10 @@ -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Represents overwrite parameter +/// If output file should be overwritten if exists +/// +public class OverwriteArgument : IArgument { - /// - /// Represents overwrite parameter - /// If output file should be overwritten if exists - /// - public class OverwriteArgument : IArgument - { - public string Text => "-y"; - } + public string Text => "-y"; } diff --git a/FFMpegCore/FFMpeg/Arguments/PadArgument.cs b/FFMpegCore/FFMpeg/Arguments/PadArgument.cs index 66dcb28..dccdb4d 100644 --- a/FFMpegCore/FFMpeg/Arguments/PadArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/PadArgument.cs @@ -1,64 +1,62 @@ 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; - - public PadArgument(PadOptions options) - { - _options = options; - } - - public string Key => "pad"; - public string Value => _options.TextInternal; - + _options = options; } - public class PadOptions + public string Key => "pad"; + public string Value => _options.TextInternal; +} + +public class PadOptions +{ + public readonly Dictionary Parameters = new(); + + private PadOptions(string? width, string? height) { - public readonly Dictionary Parameters = new(); - - internal string TextInternal => string.Join(":", Parameters.Select(parameter => parameter.FormatArgumentPair(true))); - - public static PadOptions Create(string? width, string? height) + if (width == null && height == null) { - 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); - return this; + Parameters.Add("height", height); } + } - private PadOptions(string? width, string? height) - { - if (width == null && height == null) - { - throw new Exception("At least one of the parameters must be not null"); - } + private PadOptions(string aspectRatio) + { + Parameters.Add("aspect", aspectRatio); + } - if (width != null) - { - Parameters.Add("width", width); - } + internal string TextInternal => string.Join(":", Parameters.Select(parameter => parameter.FormatArgumentPair(true))); - if (height != null) - { - Parameters.Add("height", height); - } - } + public static PadOptions Create(string? width, string? height) + { + return new PadOptions(width, height); + } - private PadOptions(string aspectRatio) - { - Parameters.Add("aspect", aspectRatio); - } + public static PadOptions Create(string aspectRatio) + { + return new PadOptions(aspectRatio); + } + + public PadOptions WithParameter(string key, string value) + { + Parameters.Add(key, value); + return this; } } diff --git a/FFMpegCore/FFMpeg/Arguments/PanArgument.cs b/FFMpegCore/FFMpeg/Arguments/PanArgument.cs index 636ad73..5b68bda 100644 --- a/FFMpegCore/FFMpeg/Arguments/PanArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/PanArgument.cs @@ -1,61 +1,60 @@ -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Mix channels with specific gain levels. +/// +public class PanArgument : IAudioFilterArgument { + private readonly string[] _outputDefinitions; + public readonly string ChannelLayout; + /// - /// Mix channels with specific gain levels. + /// Mix channels with specific gain levels /// - public class PanArgument : IAudioFilterArgument + /// + /// Represent the output channel layout. Like "stereo", "mono", "2.1", "5.1" + /// + /// + /// Output channel specification, of the form: "out_name=[gain*]in_name[(+-)[gain*]in_name...]" + /// + public PanArgument(string channelLayout, params string[] outputDefinitions) { - public readonly string ChannelLayout; - private readonly string[] _outputDefinitions; - - /// - /// Mix channels with specific gain levels - /// - /// - /// Represent the output channel layout. Like "stereo", "mono", "2.1", "5.1" - /// - /// - /// Output channel specification, of the form: "out_name=[gain*]in_name[(+-)[gain*]in_name...]" - /// - public PanArgument(string channelLayout, params string[] outputDefinitions) + if (string.IsNullOrWhiteSpace(channelLayout)) { - if (string.IsNullOrWhiteSpace(channelLayout)) - { - throw new ArgumentException("The channel layout must be set", nameof(channelLayout)); - } - - ChannelLayout = channelLayout; - - _outputDefinitions = outputDefinitions; + throw new ArgumentException("The channel layout must be set", nameof(channelLayout)); } - /// - /// Mix channels with specific gain levels - /// - /// Number of channels in output file - /// - /// Output channel specification, of the form: "out_name=[gain*]in_name[(+-)[gain*]in_name...]" - /// - public PanArgument(int channels, params string[] outputDefinitions) - { - if (channels <= 0) - { - throw new ArgumentOutOfRangeException(nameof(channels)); - } + ChannelLayout = channelLayout; - 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().Append(ChannelLayout).Concat(_outputDefinitions)); + _outputDefinitions = outputDefinitions; } + + /// + /// Mix channels with specific gain levels + /// + /// Number of channels in output file + /// + /// Output channel specification, of the form: "out_name=[gain*]in_name[(+-)[gain*]in_name...]" + /// + 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().Append(ChannelLayout).Concat(_outputDefinitions)); } diff --git a/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs b/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs index 0751c9e..311a518 100644 --- a/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs @@ -2,23 +2,28 @@ using System.IO.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; } - public string PipePath => PipeHelpers.GetPipePath(PipeName); + PipeName = PipeHelpers.GetUniquePipeName(); + _direction = direction; + } - protected NamedPipeServerStream Pipe { get; private set; } = null!; - private readonly PipeDirection _direction; + private string PipeName { get; } + public string PipePath => PipeHelpers.GetPipePath(PipeName); - protected PipeArgument(PipeDirection direction) - { - PipeName = PipeHelpers.GetUnqiuePipeName(); - _direction = direction; - } + protected NamedPipeServerStream Pipe { get; private set; } = null!; + public abstract string Text { get; } - public void Pre() + public void Pre() + { + lock (_pipeLock) { if (Pipe != null) { @@ -27,35 +32,40 @@ namespace FFMpegCore.Arguments 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 = 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 }) { Pipe.Disconnect(); } } } - - protected abstract Task ProcessDataAsync(CancellationToken token); - public abstract string Text { get; } } + + protected abstract Task ProcessDataAsync(CancellationToken token); } diff --git a/FFMpegCore/FFMpeg/Arguments/RemoveMetadataArgument.cs b/FFMpegCore/FFMpeg/Arguments/RemoveMetadataArgument.cs index 53cebad..ea7d2d2 100644 --- a/FFMpegCore/FFMpeg/Arguments/RemoveMetadataArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/RemoveMetadataArgument.cs @@ -1,10 +1,9 @@ -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Remove metadata argument +/// +public class RemoveMetadataArgument : IArgument { - /// - /// Remove metadata argument - /// - public class RemoveMetadataArgument : IArgument - { - public string Text => "-map_metadata -1"; - } + public string Text => "-map_metadata -1"; } diff --git a/FFMpegCore/FFMpeg/Arguments/ScaleArgument.cs b/FFMpegCore/FFMpeg/Arguments/ScaleArgument.cs index 6ed2b31..a75239d 100644 --- a/FFMpegCore/FFMpeg/Arguments/ScaleArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/ScaleArgument.cs @@ -1,27 +1,27 @@ using System.Drawing; using FFMpegCore.Enums; -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Represents scale parameter +/// +public class ScaleArgument : IVideoFilterArgument { - /// - /// Represents scale parameter - /// - public class ScaleArgument : IVideoFilterArgument + public readonly Size? Size; + + public ScaleArgument(Size? size) { - public readonly 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}"; + Size = size; } + + 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}"; } diff --git a/FFMpegCore/FFMpeg/Arguments/SeekArgument.cs b/FFMpegCore/FFMpeg/Arguments/SeekArgument.cs index 29cda7f..2904f52 100644 --- a/FFMpegCore/FFMpeg/Arguments/SeekArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/SeekArgument.cs @@ -1,19 +1,18 @@ using FFMpegCore.Extend; -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Represents seek parameter +/// +public class SeekArgument : IArgument { - /// - /// Represents seek parameter - /// - public class SeekArgument : IArgument + public readonly TimeSpan? SeekTo; + + public SeekArgument(TimeSpan? seekTo) { - public readonly TimeSpan? SeekTo; - - public SeekArgument(TimeSpan? seekTo) - { - SeekTo = seekTo; - } - - public string Text => SeekTo.HasValue ? $"-ss {SeekTo.Value.ToLongString()}" : string.Empty; + SeekTo = seekTo; } + + public string Text => SeekTo.HasValue ? $"-ss {SeekTo.Value.ToLongString()}" : string.Empty; } diff --git a/FFMpegCore/FFMpeg/Arguments/SetMirroringArgument.cs b/FFMpegCore/FFMpeg/Arguments/SetMirroringArgument.cs index f384cb7..10de953 100644 --- a/FFMpegCore/FFMpeg/Arguments/SetMirroringArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/SetMirroringArgument.cs @@ -1,24 +1,23 @@ 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; - } - - 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)) - }; + 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)) + }; } diff --git a/FFMpegCore/FFMpeg/Arguments/ShortestArgument.cs b/FFMpegCore/FFMpeg/Arguments/ShortestArgument.cs index d85813e..6fa9915 100644 --- a/FFMpegCore/FFMpeg/Arguments/ShortestArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/ShortestArgument.cs @@ -1,17 +1,16 @@ -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Represents shortest parameter +/// +public class ShortestArgument : IArgument { - /// - /// Represents shortest parameter - /// - public class ShortestArgument : IArgument + public readonly bool Shortest; + + public ShortestArgument(bool shortest) { - public readonly bool Shortest; - - public ShortestArgument(bool shortest) - { - Shortest = shortest; - } - - public string Text => Shortest ? "-shortest" : string.Empty; + Shortest = shortest; } + + public string Text => Shortest ? "-shortest" : string.Empty; } diff --git a/FFMpegCore/FFMpeg/Arguments/SilenceDetectArgument.cs b/FFMpegCore/FFMpeg/Arguments/SilenceDetectArgument.cs index 23c6ba3..1ac93a5 100644 --- a/FFMpegCore/FFMpeg/Arguments/SilenceDetectArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/SilenceDetectArgument.cs @@ -1,39 +1,44 @@ using System.Globalization; -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +public class SilenceDetectArgument : IAudioFilterArgument { - public class SilenceDetectArgument : IAudioFilterArgument + private readonly Dictionary _arguments = new(); + + /// + /// Silence Detection. + /// + /// Set noise type to db (decibel) or ar (amplitude ratio). Default is dB + /// + /// Set noise tolerance. Can be specified in dB (in case "dB" is appended to the specified value) or amplitude ratio. + /// Default is -60dB, or 0.001. + /// + /// + /// Set silence duration until notification (default is 2 seconds). See (ffmpeg-utils)the Time duration section in the + /// ffmpeg-utils(1) manual for the accepted syntax. + /// + /// Process each channel separately, instead of combined. By default is disabled. + public SilenceDetectArgument(string noise_type = "db", double noise = 60, double duration = 2, bool mono = false) { - private readonly Dictionary _arguments = new(); - /// - /// Silence Detection. - /// - /// Set noise type to db (decibel) or ar (amplitude ratio). Default is dB - /// Set noise tolerance. Can be specified in dB (in case "dB" is appended to the specified value) or amplitude ratio. Default is -60dB, or 0.001. - /// Set silence duration until notification (default is 2 seconds). See (ffmpeg-utils)the Time duration section in the ffmpeg-utils(1) manual for the accepted syntax. - /// Process each channel separately, instead of combined. By default is disabled. - - public SilenceDetectArgument(string noise_type = "db", double noise = 60, double duration = 2, bool mono = false) + if (noise_type == "db") { - if (noise_type == "db") - { - _arguments.Add("n", $"{noise.ToString("0.0", CultureInfo.InvariantCulture)}dB"); - } - else if (noise_type == "ar") - { - _arguments.Add("n", noise.ToString("0.00", CultureInfo.InvariantCulture)); - } - else - { - throw new ArgumentOutOfRangeException(nameof(noise_type), "Noise type must be either db or ar"); - } - - _arguments.Add("d", duration.ToString("0.00", CultureInfo.InvariantCulture)); - _arguments.Add("m", (mono ? 1 : 0).ToString()); + _arguments.Add("n", $"{noise.ToString("0.0", CultureInfo.InvariantCulture)}dB"); + } + else if (noise_type == "ar") + { + _arguments.Add("n", noise.ToString("0.00", CultureInfo.InvariantCulture)); + } + else + { + throw new ArgumentOutOfRangeException(nameof(noise_type), "Noise type must be either db or ar"); } - public string Key { get; } = "silencedetect"; - - public string Value => string.Join(":", _arguments.Select(pair => $"{pair.Key}={pair.Value}")); + _arguments.Add("d", duration.ToString("0.00", CultureInfo.InvariantCulture)); + _arguments.Add("m", (mono ? 1 : 0).ToString()); } + + public string Key { get; } = "silencedetect"; + + public string Value => string.Join(":", _arguments.Select(pair => $"{pair.Key}={pair.Value}")); } diff --git a/FFMpegCore/FFMpeg/Arguments/SizeArgument.cs b/FFMpegCore/FFMpeg/Arguments/SizeArgument.cs index 924c0a0..e68aa5e 100644 --- a/FFMpegCore/FFMpeg/Arguments/SizeArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/SizeArgument.cs @@ -1,20 +1,20 @@ using System.Drawing; -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Represents size parameter +/// +public class SizeArgument : IArgument { - /// - /// Represents size parameter - /// - public class SizeArgument : IArgument + public readonly Size? Size; + + public SizeArgument(Size? size) { - public readonly Size? Size; - public SizeArgument(Size? size) - { - Size = size; - } - - public SizeArgument(int width, int height) : this(new Size(width, height)) { } - - public string Text => Size == null ? string.Empty : $"-s {Size.Value.Width}x{Size.Value.Height}"; + Size = size; } + + public SizeArgument(int width, int height) : this(new Size(width, height)) { } + + public string Text => Size == null ? string.Empty : $"-s {Size.Value.Width}x{Size.Value.Height}"; } diff --git a/FFMpegCore/FFMpeg/Arguments/SpeedPresetArgument.cs b/FFMpegCore/FFMpeg/Arguments/SpeedPresetArgument.cs index 6046c3c..6921e87 100644 --- a/FFMpegCore/FFMpeg/Arguments/SpeedPresetArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/SpeedPresetArgument.cs @@ -1,19 +1,18 @@ using FFMpegCore.Enums; -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Represents speed parameter +/// +public class SpeedPresetArgument : IArgument { - /// - /// Represents speed parameter - /// - public class SpeedPresetArgument : IArgument + public readonly Speed Speed; + + public SpeedPresetArgument(Speed speed) { - public readonly Speed Speed; - - public SpeedPresetArgument(Speed speed) - { - Speed = speed; - } - - public string Text => $"-preset {Speed.ToString().ToLowerInvariant()}"; + Speed = speed; } + + public string Text => $"-preset {Speed.ToString().ToLowerInvariant()}"; } diff --git a/FFMpegCore/FFMpeg/Arguments/StartNumberArgument.cs b/FFMpegCore/FFMpeg/Arguments/StartNumberArgument.cs index 8e205af..2127a18 100644 --- a/FFMpegCore/FFMpeg/Arguments/StartNumberArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/StartNumberArgument.cs @@ -1,17 +1,16 @@ -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Represents start number parameter +/// +public class StartNumberArgument : IArgument { - /// - /// Represents start number parameter - /// - public class StartNumberArgument : IArgument + public readonly int StartNumber; + + public StartNumberArgument(int startNumber) { - public readonly int StartNumber; - - public StartNumberArgument(int startNumber) - { - StartNumber = startNumber; - } - - public string Text => $"-start_number {StartNumber}"; + StartNumber = startNumber; } + + public string Text => $"-start_number {StartNumber}"; } diff --git a/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs b/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs index 85db8ae..165f61e 100644 --- a/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs @@ -1,132 +1,131 @@ using System.Drawing; using FFMpegCore.Extend; -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +public class SubtitleHardBurnArgument : IVideoFilterArgument { - public class SubtitleHardBurnArgument : IVideoFilterArgument + private readonly SubtitleHardBurnOptions _subtitleHardBurnOptions; + + public SubtitleHardBurnArgument(SubtitleHardBurnOptions subtitleHardBurnOptions) { - private readonly SubtitleHardBurnOptions _subtitleHardBurnOptions; - - public SubtitleHardBurnArgument(SubtitleHardBurnOptions subtitleHardBurnOptions) - { - _subtitleHardBurnOptions = subtitleHardBurnOptions; - } - - public string Key => "subtitles"; - - public string Value => _subtitleHardBurnOptions.TextInternal; + _subtitleHardBurnOptions = subtitleHardBurnOptions; } - public class SubtitleHardBurnOptions + public string Key => "subtitles"; + + public string Value => _subtitleHardBurnOptions.TextInternal; +} + +public class SubtitleHardBurnOptions +{ + private readonly string _subtitle; + + public readonly Dictionary Parameters = new(); + + private SubtitleHardBurnOptions(string subtitle) { - private readonly string _subtitle; - - public readonly Dictionary Parameters = new(); - - /// - /// Create a new using a provided subtitle file or a video file - /// containing one. - /// - /// - /// - /// Only support .srt and .ass files, and subrip and ssa subtitle streams - public static SubtitleHardBurnOptions Create(string subtitlePath) - { - return new SubtitleHardBurnOptions(subtitlePath); - } - - private SubtitleHardBurnOptions(string subtitle) - { - _subtitle = subtitle; - } - - /// - /// Specify the size of the original video, the video for which the ASS file was composed. - /// - /// - /// - /// - public SubtitleHardBurnOptions SetOriginalSize(int width, int height) - { - return WithParameter("original_size", $"{width}x{height}"); - } - - /// - /// Specify the size of the original video, the video for which the ASS file was composed. - /// - /// - /// - public SubtitleHardBurnOptions SetOriginalSize(Size size) - { - return SetOriginalSize(size.Width, size.Height); - } - - /// - /// Set subtitles stream index. - /// - /// - /// - /// - /// Used when the provided subtitle is an stream of a video file (ex. .mkv) with multiple subtitles. - /// Represent the index of the subtitle not the stream, them the first subtitle index is 0 and second is 1 - /// - public SubtitleHardBurnOptions SetSubtitleIndex(int index) - { - return WithParameter("stream_index", index.ToString()); - } - - /// - /// Set subtitles input character encoding. Only useful if not UTF-8 - /// - /// Charset encoding - /// - public SubtitleHardBurnOptions SetCharacterEncoding(string encode) - { - return WithParameter("charenc", encode); - } - - /// - /// Override default style or script info parameters of the subtitles - /// - /// - /// - public SubtitleHardBurnOptions WithStyle(StyleOptions styleOptions) - { - return WithParameter("force_style", styleOptions.TextInternal); - } - - public SubtitleHardBurnOptions WithParameter(string key, string value) - { - Parameters.Add(key, value); - return this; - } - - internal string TextInternal => string - .Join(":", new[] { StringExtensions.EncloseInQuotes(StringExtensions.ToFFmpegLibavfilterPath(_subtitle)) } - .Concat(Parameters.Select(parameter => parameter.FormatArgumentPair(enclose: true)))); + _subtitle = subtitle; } - public class StyleOptions + internal string TextInternal => string + .Join(":", new[] { StringExtensions.EncloseInQuotes(StringExtensions.ToFFmpegLibavfilterPath(_subtitle)) } + .Concat(Parameters.Select(parameter => parameter.FormatArgumentPair(true)))); + + /// + /// Create a new using a provided subtitle file or a video file + /// containing one. + /// + /// + /// + /// Only support .srt and .ass files, and subrip and ssa subtitle streams + public static SubtitleHardBurnOptions Create(string subtitlePath) { - public readonly Dictionary Parameters = new(); + return new SubtitleHardBurnOptions(subtitlePath); + } - public static StyleOptions Create() - { - return new StyleOptions(); - } + /// + /// Specify the size of the original video, the video for which the ASS file was composed. + /// + /// + /// + /// + public SubtitleHardBurnOptions SetOriginalSize(int width, int height) + { + return WithParameter("original_size", $"{width}x{height}"); + } - /// - /// Used to override default style or script info parameters of the subtitles. It accepts ASS style format - /// - /// - /// - /// - public StyleOptions WithParameter(string key, string value) - { - Parameters.Add(key, value); - return this; - } + /// + /// Specify the size of the original video, the video for which the ASS file was composed. + /// + /// + /// + public SubtitleHardBurnOptions SetOriginalSize(Size size) + { + return SetOriginalSize(size.Width, size.Height); + } - internal string TextInternal => string.Join(",", Parameters.Select(parameter => parameter.FormatArgumentPair(enclose: false))); + /// + /// Set subtitles stream index. + /// + /// + /// + /// + /// Used when the provided subtitle is an stream of a video file (ex. .mkv) with multiple subtitles. + /// Represent the index of the subtitle not the stream, them the first subtitle index is 0 and second is 1 + /// + public SubtitleHardBurnOptions SetSubtitleIndex(int index) + { + return WithParameter("stream_index", index.ToString()); + } + + /// + /// Set subtitles input character encoding. Only useful if not UTF-8 + /// + /// Charset encoding + /// + public SubtitleHardBurnOptions SetCharacterEncoding(string encode) + { + return WithParameter("charenc", encode); + } + + /// + /// Override default style or script info parameters of the subtitles + /// + /// + /// + public SubtitleHardBurnOptions WithStyle(StyleOptions styleOptions) + { + return WithParameter("force_style", styleOptions.TextInternal); + } + + public SubtitleHardBurnOptions WithParameter(string key, string value) + { + Parameters.Add(key, value); + return this; + } +} + +public class StyleOptions +{ + public readonly Dictionary Parameters = new(); + + internal string TextInternal => string.Join(",", Parameters.Select(parameter => parameter.FormatArgumentPair(false))); + + public static StyleOptions Create() + { + return new StyleOptions(); + } + + /// + /// Used to override default style or script info parameters of the subtitles. It accepts ASS style format + /// + /// + /// + /// + public StyleOptions WithParameter(string key, string value) + { + Parameters.Add(key, value); + return this; } } diff --git a/FFMpegCore/FFMpeg/Arguments/ThreadsArgument.cs b/FFMpegCore/FFMpeg/Arguments/ThreadsArgument.cs index 5e1a208..3919ca2 100644 --- a/FFMpegCore/FFMpeg/Arguments/ThreadsArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/ThreadsArgument.cs @@ -1,19 +1,19 @@ -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Represents threads parameter +/// Number of threads used for video encoding +/// +public class ThreadsArgument : IArgument { - /// - /// Represents threads parameter - /// Number of threads used for video encoding - /// - public class ThreadsArgument : IArgument + public readonly int Threads; + + public ThreadsArgument(int threads) { - public readonly int Threads; - public ThreadsArgument(int threads) - { - Threads = threads; - } - - public ThreadsArgument(bool isMultiThreaded) : this(isMultiThreaded ? Environment.ProcessorCount : 1) { } - - public string Text => $"-threads {Threads}"; + Threads = threads; } + + public ThreadsArgument(bool isMultiThreaded) : this(isMultiThreaded ? Environment.ProcessorCount : 1) { } + + public string Text => $"-threads {Threads}"; } diff --git a/FFMpegCore/FFMpeg/Arguments/TransposeArgument.cs b/FFMpegCore/FFMpeg/Arguments/TransposeArgument.cs index e111060..8f9d9ca 100644 --- a/FFMpegCore/FFMpeg/Arguments/TransposeArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/TransposeArgument.cs @@ -1,23 +1,23 @@ using FFMpegCore.Enums; -namespace FFMpegCore.Arguments -{ - /// - /// Transpose argument. - /// 0 = 90CounterCLockwise and Vertical Flip (default) - /// 1 = 90Clockwise - /// 2 = 90CounterClockwise - /// 3 = 90Clockwise and Vertical Flip - /// - public class TransposeArgument : IVideoFilterArgument - { - public readonly Transposition Transposition; - public TransposeArgument(Transposition transposition) - { - Transposition = transposition; - } +namespace FFMpegCore.Arguments; - public string Key { get; } = "transpose"; - public string Value => ((int)Transposition).ToString(); +/// +/// Transpose argument. +/// 0 = 90CounterCLockwise and Vertical Flip (default) +/// 1 = 90Clockwise +/// 2 = 90CounterClockwise +/// 3 = 90Clockwise and Vertical Flip +/// +public class TransposeArgument : IVideoFilterArgument +{ + public readonly Transposition Transposition; + + public TransposeArgument(Transposition transposition) + { + Transposition = transposition; } + + public string Key { get; } = "transpose"; + public string Value => ((int)Transposition).ToString(); } diff --git a/FFMpegCore/FFMpeg/Arguments/VariableBitRateArgument.cs b/FFMpegCore/FFMpeg/Arguments/VariableBitRateArgument.cs index 2f169d8..031b254 100644 --- a/FFMpegCore/FFMpeg/Arguments/VariableBitRateArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/VariableBitRateArgument.cs @@ -1,22 +1,21 @@ -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Variable Bitrate Argument (VBR) argument +/// +public class VariableBitRateArgument : IArgument { - /// - /// Variable Bitrate Argument (VBR) argument - /// - public class VariableBitRateArgument : IArgument + public readonly int Vbr; + + public VariableBitRateArgument(int vbr) { - public readonly int Vbr; - - public VariableBitRateArgument(int vbr) + if (vbr < 0 || vbr > 5) { - if (vbr < 0 || vbr > 5) - { - throw new ArgumentException("Argument is outside range (0 - 5)", nameof(vbr)); - } - - Vbr = vbr; + throw new ArgumentException("Argument is outside range (0 - 5)", nameof(vbr)); } - public string Text => $"-vbr {Vbr}"; + Vbr = vbr; } + + public string Text => $"-vbr {Vbr}"; } diff --git a/FFMpegCore/FFMpeg/Arguments/VerbosityLevelArgument.cs b/FFMpegCore/FFMpeg/Arguments/VerbosityLevelArgument.cs index da236f9..b50ff03 100644 --- a/FFMpegCore/FFMpeg/Arguments/VerbosityLevelArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/VerbosityLevelArgument.cs @@ -1,25 +1,25 @@ -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +public class VerbosityLevelArgument : IArgument { - public class VerbosityLevelArgument : IArgument - { - private readonly VerbosityLevel _verbosityLevel; + private readonly VerbosityLevel _verbosityLevel; - public VerbosityLevelArgument(VerbosityLevel verbosityLevel) - { - _verbosityLevel = verbosityLevel; - } - public string Text => $"{((int)_verbosityLevel < 32 ? "-hide_banner " : "")}-loglevel {_verbosityLevel.ToString().ToLowerInvariant()}"; + public VerbosityLevelArgument(VerbosityLevel verbosityLevel) + { + _verbosityLevel = verbosityLevel; } - public enum VerbosityLevel - { - Quiet = -8, - Fatal = 8, - Error = 16, - Warning = 24, - Info = 32, - Verbose = 40, - Debug = 48, - Trace = 56 - } + public string Text => $"{((int)_verbosityLevel < 32 ? "-hide_banner " : "")}-loglevel {_verbosityLevel.ToString().ToLowerInvariant()}"; +} + +public enum VerbosityLevel +{ + Quiet = -8, + Fatal = 8, + Error = 16, + Warning = 24, + Info = 32, + Verbose = 40, + Debug = 48, + Trace = 56 } diff --git a/FFMpegCore/FFMpeg/Arguments/VideoBitrateArgument.cs b/FFMpegCore/FFMpeg/Arguments/VideoBitrateArgument.cs index 213b3d1..5416e8c 100644 --- a/FFMpegCore/FFMpeg/Arguments/VideoBitrateArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/VideoBitrateArgument.cs @@ -1,17 +1,16 @@ -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Represents video bitrate parameter +/// +public class VideoBitrateArgument : IArgument { - /// - /// Represents video bitrate parameter - /// - public class VideoBitrateArgument : IArgument + public readonly int Bitrate; + + public VideoBitrateArgument(int bitrate) { - public readonly int Bitrate; - - public VideoBitrateArgument(int bitrate) - { - Bitrate = bitrate; - } - - public string Text => $"-b:v {Bitrate}k"; + Bitrate = bitrate; } + + public string Text => $"-b:v {Bitrate}k"; } diff --git a/FFMpegCore/FFMpeg/Arguments/VideoCodecArgument.cs b/FFMpegCore/FFMpeg/Arguments/VideoCodecArgument.cs index b12afc7..3e7ecaa 100644 --- a/FFMpegCore/FFMpeg/Arguments/VideoCodecArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/VideoCodecArgument.cs @@ -1,30 +1,29 @@ using FFMpegCore.Enums; using FFMpegCore.Exceptions; -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +/// +/// Represents video codec parameter +/// +public class VideoCodecArgument : IArgument { - /// - /// Represents video codec parameter - /// - public class VideoCodecArgument : IArgument + public readonly string Codec; + + public VideoCodecArgument(string codec) { - public readonly string Codec; - - public VideoCodecArgument(string codec) - { - Codec = codec; - } - - public VideoCodecArgument(Codec value) - { - if (value.Type != CodecType.Video) - { - throw new FFMpegException(FFMpegExceptionType.Operation, $"Codec \"{value.Name}\" is not a video codec"); - } - - Codec = value.Name; - } - - public string Text => $"-c:v {Codec}"; + Codec = codec; } + + public VideoCodecArgument(Codec value) + { + if (value.Type != CodecType.Video) + { + throw new FFMpegException(FFMpegExceptionType.Operation, $"Codec \"{value.Name}\" is not a video codec"); + } + + Codec = value.Name; + } + + public string Text => $"-c:v {Codec}"; } diff --git a/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs b/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs index f7a9e4a..40d1bb1 100644 --- a/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs @@ -2,63 +2,101 @@ using FFMpegCore.Enums; using FFMpegCore.Exceptions; -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments; + +public class VideoFiltersArgument : IArgument { - public class VideoFiltersArgument : IArgument - { - public readonly VideoFilterOptions Options; + public readonly VideoFilterOptions Options; - public VideoFiltersArgument(VideoFilterOptions options) + public VideoFiltersArgument(VideoFilterOptions options) + { + Options = options; + } + + public string Text => GetText(); + + private string GetText() + { + if (!Options.Arguments.Any()) { - Options = options; + throw new FFMpegArgumentException("No video-filter arguments provided"); } - public string Text => GetText(); - - private string GetText() - { - if (!Options.Arguments.Any()) + var arguments = Options.Arguments + .Where(arg => !string.IsNullOrEmpty(arg.Value)) + .Select(arg => { - throw new FFMpegArgumentException("No video-filter arguments provided"); - } + var escapedValue = arg.Value.Replace(",", "\\,"); + return string.IsNullOrEmpty(arg.Key) ? escapedValue : $"{arg.Key}={escapedValue}"; + }); - var arguments = Options.Arguments - .Where(arg => !string.IsNullOrEmpty(arg.Value)) - .Select(arg => - { - var escapedValue = arg.Value.Replace(",", "\\,"); - return string.IsNullOrEmpty(arg.Key) ? escapedValue : $"{arg.Key}={escapedValue}"; - }); - - return $"-vf \"{string.Join(", ", arguments)}\""; - } - } - - public interface IVideoFilterArgument - { - public string Key { get; } - public string Value { get; } - } - - public class VideoFilterOptions - { - public List Arguments { get; } = new(); - - public VideoFilterOptions Scale(VideoSize videoSize) => WithArgument(new ScaleArgument(videoSize)); - public VideoFilterOptions Scale(int width, int height) => WithArgument(new ScaleArgument(width, height)); - public VideoFilterOptions Scale(Size size) => WithArgument(new ScaleArgument(size)); - public VideoFilterOptions Transpose(Transposition transposition) => WithArgument(new TransposeArgument(transposition)); - public VideoFilterOptions Mirror(Mirroring mirroring) => WithArgument(new SetMirroringArgument(mirroring)); - public VideoFilterOptions DrawText(DrawTextOptions drawTextOptions) => WithArgument(new DrawTextArgument(drawTextOptions)); - public VideoFilterOptions HardBurnSubtitle(SubtitleHardBurnOptions subtitleHardBurnOptions) => WithArgument(new SubtitleHardBurnArgument(subtitleHardBurnOptions)); - public VideoFilterOptions BlackDetect(double minimumDuration = 2.0, double pictureBlackRatioThreshold = 0.98, double pixelBlackThreshold = 0.1) => WithArgument(new BlackDetectArgument(minimumDuration, pictureBlackRatioThreshold, pixelBlackThreshold)); - public VideoFilterOptions BlackFrame(int amount = 98, int threshold = 32) => WithArgument(new BlackFrameArgument(amount, threshold)); - public VideoFilterOptions Pad(PadOptions padOptions) => WithArgument(new PadArgument(padOptions)); - - private VideoFilterOptions WithArgument(IVideoFilterArgument argument) - { - Arguments.Add(argument); - return this; - } + return $"-vf \"{string.Join(", ", arguments)}\""; + } +} + +public interface IVideoFilterArgument +{ + string Key { get; } + string Value { get; } +} + +public class VideoFilterOptions +{ + public List Arguments { get; } = new(); + + public VideoFilterOptions Scale(VideoSize videoSize) + { + return WithArgument(new ScaleArgument(videoSize)); + } + + public VideoFilterOptions Scale(int width, int height) + { + return WithArgument(new ScaleArgument(width, height)); + } + + public VideoFilterOptions Scale(Size size) + { + return WithArgument(new ScaleArgument(size)); + } + + public VideoFilterOptions Transpose(Transposition transposition) + { + return WithArgument(new TransposeArgument(transposition)); + } + + public VideoFilterOptions Mirror(Mirroring mirroring) + { + return WithArgument(new SetMirroringArgument(mirroring)); + } + + public VideoFilterOptions DrawText(DrawTextOptions drawTextOptions) + { + return WithArgument(new DrawTextArgument(drawTextOptions)); + } + + public VideoFilterOptions HardBurnSubtitle(SubtitleHardBurnOptions subtitleHardBurnOptions) + { + return WithArgument(new SubtitleHardBurnArgument(subtitleHardBurnOptions)); + } + + public VideoFilterOptions BlackDetect(double minimumDuration = 2.0, double pictureBlackRatioThreshold = 0.98, double pixelBlackThreshold = 0.1) + { + return WithArgument(new BlackDetectArgument(minimumDuration, pictureBlackRatioThreshold, pixelBlackThreshold)); + } + + public VideoFilterOptions BlackFrame(int amount = 98, int threshold = 32) + { + return WithArgument(new BlackFrameArgument(amount, threshold)); + } + + public VideoFilterOptions Pad(PadOptions padOptions) + { + return WithArgument(new PadArgument(padOptions)); + } + + private VideoFilterOptions WithArgument(IVideoFilterArgument argument) + { + Arguments.Add(argument); + return this; } } diff --git a/FFMpegCore/FFMpeg/Builders/MetaData/ChapterData.cs b/FFMpegCore/FFMpeg/Builders/MetaData/ChapterData.cs index e278408..e00e21d 100644 --- a/FFMpegCore/FFMpeg/Builders/MetaData/ChapterData.cs +++ b/FFMpegCore/FFMpeg/Builders/MetaData/ChapterData.cs @@ -1,18 +1,17 @@ -namespace FFMpegCore.Builders.MetaData +namespace FFMpegCore.Builders.MetaData; + +public class ChapterData { - public class ChapterData + public ChapterData(string title, TimeSpan start, TimeSpan end) { - public string Title { get; private set; } - public TimeSpan Start { get; private set; } - public TimeSpan End { get; private set; } - - public TimeSpan Duration => End - Start; - - public ChapterData(string title, TimeSpan start, TimeSpan end) - { - Title = title; - Start = start; - End = end; - } + Title = title; + Start = start; + End = end; } + + public string Title { get; private set; } + public TimeSpan Start { get; } + public TimeSpan End { get; } + + public TimeSpan Duration => End - Start; } diff --git a/FFMpegCore/FFMpeg/Builders/MetaData/IReadOnlyMetaData.cs b/FFMpegCore/FFMpeg/Builders/MetaData/IReadOnlyMetaData.cs index ced87d2..f5c4623 100644 --- a/FFMpegCore/FFMpeg/Builders/MetaData/IReadOnlyMetaData.cs +++ b/FFMpegCore/FFMpeg/Builders/MetaData/IReadOnlyMetaData.cs @@ -1,9 +1,7 @@ -namespace FFMpegCore.Builders.MetaData -{ +namespace FFMpegCore.Builders.MetaData; - public interface IReadOnlyMetaData - { - IReadOnlyList Chapters { get; } - IReadOnlyDictionary Entries { get; } - } +public interface IReadOnlyMetaData +{ + IReadOnlyList Chapters { get; } + IReadOnlyDictionary Entries { get; } } diff --git a/FFMpegCore/FFMpeg/Builders/MetaData/MetaData.cs b/FFMpegCore/FFMpeg/Builders/MetaData/MetaData.cs index e8fdd42..6bf478a 100644 --- a/FFMpegCore/FFMpeg/Builders/MetaData/MetaData.cs +++ b/FFMpegCore/FFMpeg/Builders/MetaData/MetaData.cs @@ -1,30 +1,29 @@ -namespace FFMpegCore.Builders.MetaData +namespace FFMpegCore.Builders.MetaData; + +public class MetaData : IReadOnlyMetaData { - public class MetaData : IReadOnlyMetaData + public MetaData() { - public Dictionary Entries { get; private set; } - public List Chapters { get; private set; } - - IReadOnlyList IReadOnlyMetaData.Chapters => Chapters; - IReadOnlyDictionary IReadOnlyMetaData.Entries => Entries; - - public MetaData() - { - Entries = new Dictionary(); - Chapters = new List(); - } - - public MetaData(MetaData cloneSource) - { - Entries = new Dictionary(cloneSource.Entries); - Chapters = cloneSource.Chapters - .Select(x => new ChapterData - ( - start: x.Start, - end: x.End, - title: x.Title - )) - .ToList(); - } + Entries = new Dictionary(); + Chapters = new List(); } + + public MetaData(MetaData cloneSource) + { + Entries = new Dictionary(cloneSource.Entries); + Chapters = cloneSource.Chapters + .Select(x => new ChapterData + ( + start: x.Start, + end: x.End, + title: x.Title + )) + .ToList(); + } + + public Dictionary Entries { get; } + public List Chapters { get; } + + IReadOnlyList IReadOnlyMetaData.Chapters => Chapters; + IReadOnlyDictionary IReadOnlyMetaData.Entries => Entries; } diff --git a/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs b/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs index 615512b..763d6fe 100644 --- a/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs +++ b/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs @@ -1,103 +1,168 @@ -namespace FFMpegCore.Builders.MetaData +namespace FFMpegCore.Builders.MetaData; + +public class MetaDataBuilder { - public class MetaDataBuilder + private readonly MetaData _metaData = new(); + + public MetaDataBuilder WithEntry(string key, string entry) { - private readonly MetaData _metaData = new(); - - public MetaDataBuilder WithEntry(string key, string entry) + if (_metaData.Entries.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value)) { - if (_metaData.Entries.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value)) - { - entry = string.Concat(value, "; ", entry); - } - - _metaData.Entries[key] = entry; - return this; + entry = string.Concat(value, "; ", entry); } - public MetaDataBuilder WithEntry(string key, params string[] values) - => WithEntry(key, string.Join("; ", values)); + _metaData.Entries[key] = entry; + return this; + } - public MetaDataBuilder WithEntry(string key, IEnumerable values) - => WithEntry(key, string.Join("; ", values)); + public MetaDataBuilder WithEntry(string key, params string[] values) + { + return WithEntry(key, string.Join("; ", values)); + } - public MetaDataBuilder AddChapter(ChapterData chapterData) + public MetaDataBuilder WithEntry(string key, IEnumerable values) + { + return WithEntry(key, string.Join("; ", values)); + } + + public MetaDataBuilder AddChapter(ChapterData chapterData) + { + _metaData.Chapters.Add(chapterData); + return this; + } + + public MetaDataBuilder AddChapters(IEnumerable values, Func chapterGetter) + { + foreach (var value in values) { - _metaData.Chapters.Add(chapterData); - return this; + var (duration, title) = chapterGetter(value); + AddChapter(duration, title); } - public MetaDataBuilder AddChapters(IEnumerable values, Func chapterGetter) - { - foreach (var value in values) - { - var (duration, title) = chapterGetter(value); - AddChapter(duration, title); - } + return this; + } - return this; - } + public MetaDataBuilder AddChapter(TimeSpan duration, string? title = null) + { + var start = _metaData.Chapters.LastOrDefault()?.End ?? TimeSpan.Zero; + var end = start + duration; + title = string.IsNullOrEmpty(title) ? $"Chapter {_metaData.Chapters.Count + 1}" : title; - public MetaDataBuilder AddChapter(TimeSpan duration, string? title = null) - { - var start = _metaData.Chapters.LastOrDefault()?.End ?? TimeSpan.Zero; - var end = start + duration; - title = string.IsNullOrEmpty(title) ? $"Chapter {_metaData.Chapters.Count + 1}" : title; + _metaData.Chapters.Add(new ChapterData + ( + start: start, + end: end, + title: title ?? string.Empty + )); - _metaData.Chapters.Add(new ChapterData - ( - start: start, - end: end, - title: title ?? string.Empty - )); + return this; + } - return this; - } + //major_brand=M4A + public MetaDataBuilder WithMajorBrand(string value) + { + return WithEntry("major_brand", value); + } - //major_brand=M4A - public MetaDataBuilder WithMajorBrand(string value) => WithEntry("major_brand", value); + //minor_version=512 + public MetaDataBuilder WithMinorVersion(string value) + { + return WithEntry("minor_version", value); + } - //minor_version=512 - public MetaDataBuilder WithMinorVersion(string value) => WithEntry("minor_version", value); + //compatible_brands=M4A isomiso2 + public MetaDataBuilder WithCompatibleBrands(string value) + { + return WithEntry("compatible_brands", value); + } - //compatible_brands=M4A isomiso2 - public MetaDataBuilder WithCompatibleBrands(string value) => WithEntry("compatible_brands", value); + //copyright=©2017 / 2019 Dennis E. Taylor / Random House Audio / Wilhelm Heyne Verlag. Übersetzung von Urban Hofstetter (P)2019 Random House Audio + public MetaDataBuilder WithCopyright(string value) + { + return WithEntry("copyright", value); + } - //copyright=©2017 / 2019 Dennis E. Taylor / Random House Audio / Wilhelm Heyne Verlag. Übersetzung von Urban Hofstetter (P)2019 Random House Audio - public MetaDataBuilder WithCopyright(string value) => WithEntry("copyright", value); + //title=Alle diese Welten: Bobiverse 3 + public MetaDataBuilder WithTitle(string value) + { + return WithEntry("title", value); + } - //title=Alle diese Welten: Bobiverse 3 - public MetaDataBuilder WithTitle(string value) => WithEntry("title", value); + //artist=Dennis E. Taylor + public MetaDataBuilder WithArtists(params string[] value) + { + return WithEntry("artist", value); + } - //artist=Dennis E. Taylor - public MetaDataBuilder WithArtists(params string[] value) => WithEntry("artist", value); - public MetaDataBuilder WithArtists(IEnumerable value) => WithEntry("artist", value); + public MetaDataBuilder WithArtists(IEnumerable value) + { + return WithEntry("artist", value); + } - //composer=J. K. Rowling - public MetaDataBuilder WithComposers(params string[] value) => WithEntry("composer", value); - public MetaDataBuilder WithComposers(IEnumerable value) => WithEntry("composer", value); + //composer=J. K. Rowling + public MetaDataBuilder WithComposers(params string[] value) + { + return WithEntry("composer", value); + } - //album_artist=Dennis E. Taylor - public MetaDataBuilder WithAlbumArtists(params string[] value) => WithEntry("album_artist", value); - public MetaDataBuilder WithAlbumArtists(IEnumerable value) => WithEntry("album_artist", value); + public MetaDataBuilder WithComposers(IEnumerable value) + { + return WithEntry("composer", value); + } - //album=Alle diese Welten: Bobiverse 3 - public MetaDataBuilder WithAlbum(string value) => WithEntry("album", value); + //album_artist=Dennis E. Taylor + public MetaDataBuilder WithAlbumArtists(params string[] value) + { + return WithEntry("album_artist", value); + } - //date=2019 - public MetaDataBuilder WithDate(string value) => WithEntry("date", value); + public MetaDataBuilder WithAlbumArtists(IEnumerable value) + { + return WithEntry("album_artist", value); + } - //genre=Hörbuch - public MetaDataBuilder WithGenres(params string[] value) => WithEntry("genre", value); - public MetaDataBuilder WithGenres(IEnumerable value) => WithEntry("genre", value); + //album=Alle diese Welten: Bobiverse 3 + public MetaDataBuilder WithAlbum(string value) + { + return WithEntry("album", value); + } - //comment=Chapter 200 - public MetaDataBuilder WithComments(params string[] value) => WithEntry("comment", value); - public MetaDataBuilder WithComments(IEnumerable value) => WithEntry("comment", value); + //date=2019 + public MetaDataBuilder WithDate(string value) + { + return WithEntry("date", value); + } - //encoder=Lavf58.47.100 - public MetaDataBuilder WithEncoder(string value) => WithEntry("encoder", value); + //genre=Hörbuch + public MetaDataBuilder WithGenres(params string[] value) + { + return WithEntry("genre", value); + } - public ReadOnlyMetaData Build() => new(_metaData); + public MetaDataBuilder WithGenres(IEnumerable value) + { + return WithEntry("genre", value); + } + + //comment=Chapter 200 + public MetaDataBuilder WithComments(params string[] value) + { + return WithEntry("comment", value); + } + + public MetaDataBuilder WithComments(IEnumerable value) + { + return WithEntry("comment", value); + } + + //encoder=Lavf58.47.100 + public MetaDataBuilder WithEncoder(string value) + { + return WithEntry("encoder", value); + } + + public ReadOnlyMetaData Build() + { + return new ReadOnlyMetaData(_metaData); } } diff --git a/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataSerializer.cs b/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataSerializer.cs index 8db1876..6a9f64d 100644 --- a/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataSerializer.cs +++ b/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataSerializer.cs @@ -1,37 +1,36 @@ using System.Text; -namespace FFMpegCore.Builders.MetaData +namespace FFMpegCore.Builders.MetaData; + +public class MetaDataSerializer { - public class MetaDataSerializer + public static readonly MetaDataSerializer Instance = new(); + + public string Serialize(IReadOnlyMetaData metaData) { - public static readonly MetaDataSerializer Instance = new(); + var sb = new StringBuilder() + .AppendLine(";FFMETADATA1"); - public string Serialize(IReadOnlyMetaData metaData) + foreach (var value in metaData.Entries) { - var sb = new StringBuilder() - .AppendLine(";FFMETADATA1"); - - foreach (var value in metaData.Entries) - { - sb.AppendLine($"{value.Key}={value.Value}"); - } - - var chapterNumber = 0; - foreach (var chapter in metaData.Chapters ?? Enumerable.Empty()) - { - chapterNumber++; - var title = string.IsNullOrEmpty(chapter.Title) ? $"Chapter {chapterNumber}" : chapter.Title; - - sb - .AppendLine("[CHAPTER]") - .AppendLine($"TIMEBASE=1/1000") - .AppendLine($"START={(int)chapter.Start.TotalMilliseconds}") - .AppendLine($"END={(int)chapter.End.TotalMilliseconds}") - .AppendLine($"title={title}") - ; - } - - return sb.ToString(); + sb.AppendLine($"{value.Key}={value.Value}"); } + + var chapterNumber = 0; + foreach (var chapter in metaData.Chapters ?? Enumerable.Empty()) + { + chapterNumber++; + var title = string.IsNullOrEmpty(chapter.Title) ? $"Chapter {chapterNumber}" : chapter.Title; + + sb + .AppendLine("[CHAPTER]") + .AppendLine("TIMEBASE=1/1000") + .AppendLine($"START={(int)chapter.Start.TotalMilliseconds}") + .AppendLine($"END={(int)chapter.End.TotalMilliseconds}") + .AppendLine($"title={title}") + ; + } + + return sb.ToString(); } } diff --git a/FFMpegCore/FFMpeg/Builders/MetaData/ReadOnlyMetaData.cs b/FFMpegCore/FFMpeg/Builders/MetaData/ReadOnlyMetaData.cs index cf29f94..aaf8c42 100644 --- a/FFMpegCore/FFMpeg/Builders/MetaData/ReadOnlyMetaData.cs +++ b/FFMpegCore/FFMpeg/Builders/MetaData/ReadOnlyMetaData.cs @@ -1,22 +1,21 @@ -namespace FFMpegCore.Builders.MetaData -{ - public class ReadOnlyMetaData : IReadOnlyMetaData - { - public IReadOnlyDictionary Entries { get; private set; } - public IReadOnlyList Chapters { get; private set; } +namespace FFMpegCore.Builders.MetaData; - public ReadOnlyMetaData(MetaData metaData) - { - Entries = new Dictionary(metaData.Entries); - Chapters = metaData.Chapters - .Select(x => new ChapterData - ( - start: x.Start, - end: x.End, - title: x.Title - )) - .ToList() - .AsReadOnly(); - } +public class ReadOnlyMetaData : IReadOnlyMetaData +{ + public ReadOnlyMetaData(MetaData metaData) + { + Entries = new Dictionary(metaData.Entries); + Chapters = metaData.Chapters + .Select(x => new ChapterData + ( + start: x.Start, + end: x.End, + title: x.Title + )) + .ToList() + .AsReadOnly(); } + + public IReadOnlyDictionary Entries { get; } + public IReadOnlyList Chapters { get; } } diff --git a/FFMpegCore/FFMpeg/Enums/AudioQuality.cs b/FFMpegCore/FFMpeg/Enums/AudioQuality.cs index 59ed3e6..620d85c 100644 --- a/FFMpegCore/FFMpeg/Enums/AudioQuality.cs +++ b/FFMpegCore/FFMpeg/Enums/AudioQuality.cs @@ -1,12 +1,11 @@ -namespace FFMpegCore.Enums +namespace FFMpegCore.Enums; + +public enum AudioQuality { - public enum AudioQuality - { - Ultra = 384, - VeryHigh = 256, - Good = 192, - Normal = 128, - BelowNormal = 96, - Low = 64 - } + Ultra = 384, + VeryHigh = 256, + Good = 192, + Normal = 128, + BelowNormal = 96, + Low = 64 } diff --git a/FFMpegCore/FFMpeg/Enums/Codec.cs b/FFMpegCore/FFMpeg/Enums/Codec.cs index efd4196..78ff588 100644 --- a/FFMpegCore/FFMpeg/Enums/Codec.cs +++ b/FFMpegCore/FFMpeg/Enums/Codec.cs @@ -1,155 +1,156 @@ using System.Text.RegularExpressions; using FFMpegCore.Exceptions; -namespace FFMpegCore.Enums +namespace FFMpegCore.Enums; + +public enum FeatureStatus { - public enum FeatureStatus + Unknown, + NotSupported, + Supported +} + +public class Codec +{ + private static readonly Regex _codecsFormatRegex = new(@"([D\.])([E\.])([VASD\.])([I\.])([L\.])([S\.])\s+([a-z0-9_-]+)\s+(.+)"); + private static readonly Regex _decodersEncodersFormatRegex = new(@"([VASD\.])([F\.])([S\.])([X\.])([B\.])([D\.])\s+([a-z0-9_-]+)\s+(.+)"); + + internal Codec(string name, CodecType type) { - Unknown, - NotSupported, - Supported, + EncoderFeatureLevel = new FeatureLevel(); + DecoderFeatureLevel = new FeatureLevel(); + Name = name; + Type = type; } - public class Codec + public string Name { get; } + public CodecType Type { get; private set; } + public bool DecodingSupported { get; private set; } + public bool EncodingSupported { get; private set; } + public bool IsIntraFrameOnly { get; private set; } + public bool IsLossy { get; private set; } + public bool IsLossless { get; private set; } + public string Description { get; private set; } = null!; + + public FeatureLevel EncoderFeatureLevel { get; } + public FeatureLevel DecoderFeatureLevel { get; } + + internal static bool TryParseFromCodecs(string line, out Codec codec) { - private static readonly Regex _codecsFormatRegex = new(@"([D\.])([E\.])([VASD\.])([I\.])([L\.])([S\.])\s+([a-z0-9_-]+)\s+(.+)"); - private static readonly Regex _decodersEncodersFormatRegex = new(@"([VASD\.])([F\.])([S\.])([X\.])([B\.])([D\.])\s+([a-z0-9_-]+)\s+(.+)"); - - public class FeatureLevel + var match = _codecsFormatRegex.Match(line); + if (!match.Success) { - public bool IsExperimental { get; internal set; } - public FeatureStatus SupportsFrameLevelMultithreading { get; internal set; } - public FeatureStatus SupportsSliceLevelMultithreading { get; internal set; } - public FeatureStatus SupportsDrawHorizBand { get; internal set; } - public FeatureStatus SupportsDirectRendering { get; internal set; } - - internal void Merge(FeatureLevel other) - { - IsExperimental |= other.IsExperimental; - SupportsFrameLevelMultithreading = (FeatureStatus)Math.Max((int)SupportsFrameLevelMultithreading, (int)other.SupportsFrameLevelMultithreading); - SupportsSliceLevelMultithreading = (FeatureStatus)Math.Max((int)SupportsSliceLevelMultithreading, (int)other.SupportsSliceLevelMultithreading); - SupportsDrawHorizBand = (FeatureStatus)Math.Max((int)SupportsDrawHorizBand, (int)other.SupportsDrawHorizBand); - SupportsDirectRendering = (FeatureStatus)Math.Max((int)SupportsDirectRendering, (int)other.SupportsDirectRendering); - } + codec = null!; + return false; } - public string Name { get; private set; } - public CodecType Type { get; private set; } - public bool DecodingSupported { get; private set; } - public bool EncodingSupported { get; private set; } - public bool IsIntraFrameOnly { get; private set; } - public bool IsLossy { get; private set; } - public bool IsLossless { get; private set; } - public string Description { get; private set; } = null!; - - public FeatureLevel EncoderFeatureLevel { get; private set; } - public FeatureLevel DecoderFeatureLevel { get; private set; } - - internal Codec(string name, CodecType type) + var name = match.Groups[7].Value; + var type = match.Groups[3].Value switch { - EncoderFeatureLevel = new FeatureLevel(); - DecoderFeatureLevel = new FeatureLevel(); - Name = name; - Type = type; + "V" => CodecType.Video, + "A" => CodecType.Audio, + "D" => CodecType.Data, + "S" => CodecType.Subtitle, + _ => CodecType.Unknown + }; + + if (type == CodecType.Unknown) + { + codec = null!; + return false; } - internal static bool TryParseFromCodecs(string line, out Codec codec) + codec = new Codec(name, type); + + codec.DecodingSupported = match.Groups[1].Value != "."; + codec.EncodingSupported = match.Groups[2].Value != "."; + codec.IsIntraFrameOnly = match.Groups[4].Value != "."; + codec.IsLossy = match.Groups[5].Value != "."; + codec.IsLossless = match.Groups[6].Value != "."; + codec.Description = match.Groups[8].Value; + + return true; + } + + internal static bool TryParseFromEncodersDecoders(string line, out Codec codec, bool isEncoder) + { + var match = _decodersEncodersFormatRegex.Match(line); + if (!match.Success) { - var match = _codecsFormatRegex.Match(line); - if (!match.Success) - { - codec = null!; - return false; - } - - var name = match.Groups[7].Value; - var type = match.Groups[3].Value switch - { - "V" => CodecType.Video, - "A" => CodecType.Audio, - "D" => CodecType.Data, - "S" => CodecType.Subtitle, - _ => CodecType.Unknown - }; - - if (type == CodecType.Unknown) - { - codec = null!; - return false; - } - - codec = new Codec(name, type); - - codec.DecodingSupported = match.Groups[1].Value != "."; - codec.EncodingSupported = match.Groups[2].Value != "."; - codec.IsIntraFrameOnly = match.Groups[4].Value != "."; - codec.IsLossy = match.Groups[5].Value != "."; - codec.IsLossless = match.Groups[6].Value != "."; - codec.Description = match.Groups[8].Value; - - return true; + codec = null!; + return false; } - internal static bool TryParseFromEncodersDecoders(string line, out Codec codec, bool isEncoder) + + var name = match.Groups[7].Value; + var type = match.Groups[1].Value switch { - var match = _decodersEncodersFormatRegex.Match(line); - if (!match.Success) - { - codec = null!; - return false; - } + "V" => CodecType.Video, + "A" => CodecType.Audio, + "D" => CodecType.Data, + "S" => CodecType.Subtitle, + _ => CodecType.Unknown + }; - var name = match.Groups[7].Value; - var type = match.Groups[1].Value switch - { - "V" => CodecType.Video, - "A" => CodecType.Audio, - "D" => CodecType.Data, - "S" => CodecType.Subtitle, - _ => CodecType.Unknown - }; - - if (type == CodecType.Unknown) - { - codec = null!; - return false; - } - - codec = new Codec(name, type); - - var featureLevel = isEncoder ? codec.EncoderFeatureLevel : codec.DecoderFeatureLevel; - - codec.DecodingSupported = !isEncoder; - codec.EncodingSupported = isEncoder; - featureLevel.SupportsFrameLevelMultithreading = match.Groups[2].Value != "." ? FeatureStatus.Supported : FeatureStatus.NotSupported; - featureLevel.SupportsSliceLevelMultithreading = match.Groups[3].Value != "." ? FeatureStatus.Supported : FeatureStatus.NotSupported; - featureLevel.IsExperimental = match.Groups[4].Value != "."; - featureLevel.SupportsDrawHorizBand = match.Groups[5].Value != "." ? FeatureStatus.Supported : FeatureStatus.NotSupported; - featureLevel.SupportsDirectRendering = match.Groups[6].Value != "." ? FeatureStatus.Supported : FeatureStatus.NotSupported; - codec.Description = match.Groups[8].Value; - - return true; + if (type == CodecType.Unknown) + { + codec = null!; + return false; } - internal void Merge(Codec other) + + codec = new Codec(name, type); + + var featureLevel = isEncoder ? codec.EncoderFeatureLevel : codec.DecoderFeatureLevel; + + codec.DecodingSupported = !isEncoder; + codec.EncodingSupported = isEncoder; + featureLevel.SupportsFrameLevelMultithreading = match.Groups[2].Value != "." ? FeatureStatus.Supported : FeatureStatus.NotSupported; + featureLevel.SupportsSliceLevelMultithreading = match.Groups[3].Value != "." ? FeatureStatus.Supported : FeatureStatus.NotSupported; + featureLevel.IsExperimental = match.Groups[4].Value != "."; + featureLevel.SupportsDrawHorizBand = match.Groups[5].Value != "." ? FeatureStatus.Supported : FeatureStatus.NotSupported; + featureLevel.SupportsDirectRendering = match.Groups[6].Value != "." ? FeatureStatus.Supported : FeatureStatus.NotSupported; + codec.Description = match.Groups[8].Value; + + return true; + } + + internal void Merge(Codec other) + { + if (Name != other.Name) { - if (Name != other.Name) - { - throw new FFMpegException(FFMpegExceptionType.Operation, "different codecs enable to merge"); - } + throw new FFMpegException(FFMpegExceptionType.Operation, "different codecs enable to merge"); + } - Type |= other.Type; - DecodingSupported |= other.DecodingSupported; - EncodingSupported |= other.EncodingSupported; - IsIntraFrameOnly |= other.IsIntraFrameOnly; - IsLossy |= other.IsLossy; - IsLossless |= other.IsLossless; + Type |= other.Type; + DecodingSupported |= other.DecodingSupported; + EncodingSupported |= other.EncodingSupported; + IsIntraFrameOnly |= other.IsIntraFrameOnly; + IsLossy |= other.IsLossy; + IsLossless |= other.IsLossless; - EncoderFeatureLevel.Merge(other.EncoderFeatureLevel); - DecoderFeatureLevel.Merge(other.DecoderFeatureLevel); + EncoderFeatureLevel.Merge(other.EncoderFeatureLevel); + DecoderFeatureLevel.Merge(other.DecoderFeatureLevel); - if (Description != other.Description) - { - Description += "\r\n" + other.Description; - } + if (Description != other.Description) + { + Description += "\r\n" + other.Description; + } + } + + public class FeatureLevel + { + public bool IsExperimental { get; internal set; } + public FeatureStatus SupportsFrameLevelMultithreading { get; internal set; } + public FeatureStatus SupportsSliceLevelMultithreading { get; internal set; } + public FeatureStatus SupportsDrawHorizBand { get; internal set; } + public FeatureStatus SupportsDirectRendering { get; internal set; } + + internal void Merge(FeatureLevel other) + { + IsExperimental |= other.IsExperimental; + SupportsFrameLevelMultithreading = (FeatureStatus)Math.Max((int)SupportsFrameLevelMultithreading, (int)other.SupportsFrameLevelMultithreading); + SupportsSliceLevelMultithreading = (FeatureStatus)Math.Max((int)SupportsSliceLevelMultithreading, (int)other.SupportsSliceLevelMultithreading); + SupportsDrawHorizBand = (FeatureStatus)Math.Max((int)SupportsDrawHorizBand, (int)other.SupportsDrawHorizBand); + SupportsDirectRendering = (FeatureStatus)Math.Max((int)SupportsDirectRendering, (int)other.SupportsDirectRendering); } } } diff --git a/FFMpegCore/FFMpeg/Enums/ContainerFormat.cs b/FFMpegCore/FFMpeg/Enums/ContainerFormat.cs index 53c5e1a..166c202 100644 --- a/FFMpegCore/FFMpeg/Enums/ContainerFormat.cs +++ b/FFMpegCore/FFMpeg/Enums/ContainerFormat.cs @@ -1,50 +1,49 @@ using System.Text.RegularExpressions; -namespace FFMpegCore.Enums +namespace FFMpegCore.Enums; + +public class ContainerFormat { - public class ContainerFormat + private static readonly Regex FormatRegex = new(@"([D ])([E ])\s+([a-z0-9_]+)\s+(.+)"); + + internal ContainerFormat(string name) { - private static readonly Regex FormatRegex = new(@"([D ])([E ])\s+([a-z0-9_]+)\s+(.+)"); + Name = name; + } - public string Name { get; private set; } - public bool DemuxingSupported { get; private set; } - public bool MuxingSupported { get; private set; } - public string Description { get; private set; } = null!; + public string Name { get; } + public bool DemuxingSupported { get; private set; } + public bool MuxingSupported { get; private set; } + public string Description { get; private set; } = null!; - public string Extension + public string Extension + { + get { - get + if (GlobalFFOptions.Current.ExtensionOverrides.ContainsKey(Name)) { - if (GlobalFFOptions.Current.ExtensionOverrides.ContainsKey(Name)) - { - return GlobalFFOptions.Current.ExtensionOverrides[Name]; - } - - return "." + Name; - } - } - - internal ContainerFormat(string name) - { - Name = name; - } - - internal static bool TryParse(string line, out ContainerFormat fmt) - { - var match = FormatRegex.Match(line); - if (!match.Success) - { - fmt = null!; - return false; + return GlobalFFOptions.Current.ExtensionOverrides[Name]; } - fmt = new ContainerFormat(match.Groups[3].Value) - { - DemuxingSupported = match.Groups[1].Value != " ", - MuxingSupported = match.Groups[2].Value != " ", - Description = match.Groups[4].Value - }; - return true; + return "." + Name; } } + + internal static bool TryParse(string line, out ContainerFormat fmt) + { + var match = FormatRegex.Match(line); + if (!match.Success) + { + fmt = null!; + return false; + } + + fmt = new ContainerFormat(match.Groups[3].Value) + { + DemuxingSupported = match.Groups[1].Value != " ", + MuxingSupported = match.Groups[2].Value != " ", + Description = match.Groups[4].Value + }; + return true; + } } diff --git a/FFMpegCore/FFMpeg/Enums/Enums.cs b/FFMpegCore/FFMpeg/Enums/Enums.cs index 1f00203..35ceccc 100644 --- a/FFMpegCore/FFMpeg/Enums/Enums.cs +++ b/FFMpegCore/FFMpeg/Enums/Enums.cs @@ -1,90 +1,113 @@ -namespace FFMpegCore.Enums +namespace FFMpegCore.Enums; + +public enum CodecType { - public enum CodecType - { - Unknown = 0, - Video = 1 << 1, - Audio = 1 << 2, - Subtitle = 1 << 3, - Data = 1 << 4, - } + Unknown = 0, + Video = 1 << 1, + Audio = 1 << 2, + Subtitle = 1 << 3, + Data = 1 << 4 +} - public static class VideoCodec +public static class VideoCodec +{ + public static Codec LibX264 => FFMpeg.GetCodec("libx264"); + public static Codec LibX265 => FFMpeg.GetCodec("libx265"); + public static Codec LibVpx => FFMpeg.GetCodec("libvpx"); + public static Codec LibTheora => FFMpeg.GetCodec("libtheora"); + public static Codec MpegTs => FFMpeg.GetCodec("mpegts"); + public static Codec LibaomAv1 => FFMpeg.GetCodec("libaom-av1"); + + public static class Image { - public static Codec LibX264 => FFMpeg.GetCodec("libx264"); - public static Codec LibX265 => FFMpeg.GetCodec("libx265"); - public static Codec LibVpx => FFMpeg.GetCodec("libvpx"); - public static Codec LibTheora => FFMpeg.GetCodec("libtheora"); public static Codec Png => FFMpeg.GetCodec("png"); - public static Codec MpegTs => FFMpeg.GetCodec("mpegts"); - public static Codec LibaomAv1 => FFMpeg.GetCodec("libaom-av1"); - } + public static Codec Jpg => FFMpeg.GetCodec("mjpeg"); + public static Codec Bmp => FFMpeg.GetCodec("bmp"); + public static Codec Webp => FFMpeg.GetCodec("webp"); - public static class AudioCodec - { - public static Codec Aac => FFMpeg.GetCodec("aac"); - public static Codec LibVorbis => FFMpeg.GetCodec("libvorbis"); - public static Codec LibFdk_Aac => FFMpeg.GetCodec("libfdk_aac"); - public static Codec Ac3 => FFMpeg.GetCodec("ac3"); - public static Codec Eac3 => FFMpeg.GetCodec("eac3"); - public static Codec LibMp3Lame => FFMpeg.GetCodec("libmp3lame"); - public static Codec Copy => new Codec("copy", CodecType.Audio); - - } - - public static class VideoType - { - public static ContainerFormat MpegTs => FFMpeg.GetContainerFormat("mpegts"); - public static ContainerFormat Ts => FFMpeg.GetContainerFormat("mpegts"); - public static ContainerFormat Mp4 => FFMpeg.GetContainerFormat("mp4"); - public static ContainerFormat Mov => FFMpeg.GetContainerFormat("mov"); - public static ContainerFormat Avi => FFMpeg.GetContainerFormat("avi"); - public static ContainerFormat Ogv => FFMpeg.GetContainerFormat("ogv"); - public static ContainerFormat WebM => FFMpeg.GetContainerFormat("webm"); - } - - public enum Filter - { - H264_Mp4ToAnnexB, - Aac_AdtstoAsc - } - - /// - /// https://ffmpeg.org/ffmpeg.html#Stream-specifiers-1 - /// ’v’ or ’V’ for video, ’a’ for audio, ’s’ for subtitle, ’d’ for data, and ’t’ for attachments - /// ’V’ only matches video streams which are not attached pictures, video thumbnails or cover arts. - /// Both for audio + video - /// All for all types - /// - public enum Channel - { - Audio, - Video, - Both, - VideoNoAttachedPic, - Subtitle, - Data, - Attachments, - All - } - internal static class ChannelMethods - { - /// - /// is left as empty because it cannot be in a single stream specifier - /// - /// The stream_type used in stream specifiers - public static string StreamType(this Channel channel) + public static Codec GetByExtension(string path) { - return channel switch + var ext = Path.GetExtension(path); + switch (ext) { - Channel.Audio => ":a", - Channel.Video => ":v", - Channel.VideoNoAttachedPic => ":V", - Channel.Subtitle => ":s", - Channel.Data => ":d", - Channel.Attachments => ":t", - _ => string.Empty - }; + case FileExtension.Image.Png: + return Png; + case FileExtension.Image.Jpg: + return Jpg; + case FileExtension.Image.Bmp: + return Bmp; + case FileExtension.Image.Webp: + return Webp; + default: throw new NotSupportedException($"Unsupported image extension: {ext}"); + } } } } + +public static class AudioCodec +{ + public static Codec Aac => FFMpeg.GetCodec("aac"); + public static Codec LibVorbis => FFMpeg.GetCodec("libvorbis"); + public static Codec LibFdk_Aac => FFMpeg.GetCodec("libfdk_aac"); + public static Codec Ac3 => FFMpeg.GetCodec("ac3"); + public static Codec Eac3 => FFMpeg.GetCodec("eac3"); + public static Codec LibMp3Lame => FFMpeg.GetCodec("libmp3lame"); + public static Codec Copy => new("copy", CodecType.Audio); +} + +public static class VideoType +{ + public static ContainerFormat MpegTs => FFMpeg.GetContainerFormat("mpegts"); + public static ContainerFormat Ts => FFMpeg.GetContainerFormat("mpegts"); + public static ContainerFormat Mp4 => FFMpeg.GetContainerFormat("mp4"); + public static ContainerFormat Mov => FFMpeg.GetContainerFormat("mov"); + public static ContainerFormat Avi => FFMpeg.GetContainerFormat("avi"); + public static ContainerFormat Ogv => FFMpeg.GetContainerFormat("ogv"); + public static ContainerFormat WebM => FFMpeg.GetContainerFormat("webm"); +} + +public enum Filter +{ + H264_Mp4ToAnnexB, + Aac_AdtstoAsc +} + +/// +/// https://ffmpeg.org/ffmpeg.html#Stream-specifiers-1 +/// ’v’ or ’V’ for video, ’a’ for audio, ’s’ for subtitle, ’d’ for data, and ’t’ for attachments +/// ’V’ only matches video streams which are not attached pictures, video thumbnails or cover arts. +/// Both for audio + video +/// All for all types +/// +public enum Channel +{ + Audio, + Video, + Both, + VideoNoAttachedPic, + Subtitle, + Data, + Attachments, + All +} + +internal static class ChannelMethods +{ + /// + /// is left as empty because it cannot be in a single stream specifier + /// + /// The stream_type used in stream specifiers + public static string StreamType(this Channel channel) + { + return channel switch + { + Channel.Audio => ":a", + Channel.Video => ":v", + Channel.VideoNoAttachedPic => ":V", + Channel.Subtitle => ":s", + Channel.Data => ":d", + Channel.Attachments => ":t", + _ => string.Empty + }; + } +} diff --git a/FFMpegCore/FFMpeg/Enums/FFMpegLogLevel.cs b/FFMpegCore/FFMpeg/Enums/FFMpegLogLevel.cs index aa2ca23..9d2b63a 100644 --- a/FFMpegCore/FFMpeg/Enums/FFMpegLogLevel.cs +++ b/FFMpegCore/FFMpeg/Enums/FFMpegLogLevel.cs @@ -1,15 +1,14 @@ -namespace FFMpegCore.Enums +namespace FFMpegCore.Enums; + +public enum FFMpegLogLevel { - public enum FFMpegLogLevel - { - Quiet = 0, - Panic = 1, - Fatal = 2, - Error = 3, - Warning = 4, - Info = 5, - Verbose = 6, - Debug = 7, - Trace = 8 - } + Quiet = 0, + Panic = 1, + Fatal = 2, + Error = 3, + Warning = 4, + Info = 5, + Verbose = 6, + Debug = 7, + Trace = 8 } diff --git a/FFMpegCore/FFMpeg/Enums/FileExtension.cs b/FFMpegCore/FFMpeg/Enums/FileExtension.cs index f3067ba..466c06f 100644 --- a/FFMpegCore/FFMpeg/Enums/FileExtension.cs +++ b/FFMpegCore/FFMpeg/Enums/FileExtension.cs @@ -1,25 +1,36 @@ -namespace FFMpegCore.Enums +namespace FFMpegCore.Enums; + +public static class FileExtension { - public static class FileExtension + public static readonly string Mp4 = VideoType.Mp4.Extension; + public static readonly string Ts = VideoType.MpegTs.Extension; + public static readonly string Ogv = VideoType.Ogv.Extension; + public static readonly string WebM = VideoType.WebM.Extension; + public static readonly string Mp3 = ".mp3"; + public static readonly string Gif = ".gif"; + + public static string Extension(this Codec type) { - public static string Extension(this Codec type) + return type.Name switch { - return type.Name switch - { - "libx264" => Mp4, - "libxvpx" => WebM, - "libxtheora" => Ogv, - "mpegts" => Ts, - "png" => Png, - _ => throw new Exception("The extension for this video type is not defined.") - }; - } - public static readonly string Mp4 = VideoType.Mp4.Extension; - public static readonly string Ts = VideoType.MpegTs.Extension; - public static readonly string Ogv = VideoType.Ogv.Extension; - public static readonly string WebM = VideoType.WebM.Extension; - public static readonly string Png = ".png"; - public static readonly string Mp3 = ".mp3"; - public static readonly string Gif = ".gif"; + "libx264" => Mp4, + "libxvpx" => WebM, + "libxtheora" => Ogv, + "mpegts" => Ts, + "png" => Image.Png, + "jpg" => Image.Jpg, + "bmp" => Image.Bmp, + "webp" => Image.Webp, + _ => throw new Exception("The extension for this video type is not defined.") + }; + } + + public static class Image + { + public const string Png = ".png"; + public const string Jpg = ".jpg"; + public const string Bmp = ".bmp"; + public const string Webp = ".webp"; + public static readonly List All = [Png, Jpg, Bmp, Webp]; } } diff --git a/FFMpegCore/FFMpeg/Enums/HardwareAccelerationDevice.cs b/FFMpegCore/FFMpeg/Enums/HardwareAccelerationDevice.cs index 4c5dbd7..8ec92f6 100644 --- a/FFMpegCore/FFMpeg/Enums/HardwareAccelerationDevice.cs +++ b/FFMpegCore/FFMpeg/Enums/HardwareAccelerationDevice.cs @@ -1,15 +1,14 @@ -namespace FFMpegCore.Enums +namespace FFMpegCore.Enums; + +public enum HardwareAccelerationDevice { - public enum HardwareAccelerationDevice - { - Auto, - D3D11VA, - DXVA2, - QSV, - CUVID, - CUDA, - VDPAU, - VAAPI, - LibMFX - } + Auto, + D3D11VA, + DXVA2, + QSV, + CUVID, + CUDA, + VDPAU, + VAAPI, + LibMFX } diff --git a/FFMpegCore/FFMpeg/Enums/Mirroring.cs b/FFMpegCore/FFMpeg/Enums/Mirroring.cs index 5768163..9f81436 100644 --- a/FFMpegCore/FFMpeg/Enums/Mirroring.cs +++ b/FFMpegCore/FFMpeg/Enums/Mirroring.cs @@ -1,8 +1,7 @@ -namespace FFMpegCore.Enums +namespace FFMpegCore.Enums; + +public enum Mirroring { - public enum Mirroring - { - Vertical, - Horizontal - } + Vertical, + Horizontal } diff --git a/FFMpegCore/FFMpeg/Enums/PixelFormat.cs b/FFMpegCore/FFMpeg/Enums/PixelFormat.cs index 0d89f4c..305671d 100644 --- a/FFMpegCore/FFMpeg/Enums/PixelFormat.cs +++ b/FFMpegCore/FFMpeg/Enums/PixelFormat.cs @@ -1,59 +1,58 @@ using System.Text.RegularExpressions; -namespace FFMpegCore.Enums +namespace FFMpegCore.Enums; + +public class PixelFormat { - public class PixelFormat + private static readonly Regex _formatRegex = new(@"([I\.])([O\.])([H\.])([P\.])([B\.])\s+(\S+)\s+([0-9]+)\s+([0-9]+)"); + + internal PixelFormat(string name) { - private static readonly Regex _formatRegex = new(@"([I\.])([O\.])([H\.])([P\.])([B\.])\s+(\S+)\s+([0-9]+)\s+([0-9]+)"); + Name = name; + } - public bool InputConversionSupported { get; private set; } - public bool OutputConversionSupported { get; private set; } - public bool HardwareAccelerationSupported { get; private set; } - public bool IsPaletted { get; private set; } - public bool IsBitstream { get; private set; } - public string Name { get; private set; } - public int Components { get; private set; } - public int BitsPerPixel { get; private set; } + public bool InputConversionSupported { get; private set; } + public bool OutputConversionSupported { get; private set; } + public bool HardwareAccelerationSupported { get; private set; } + public bool IsPaletted { get; private set; } + public bool IsBitstream { get; private set; } + public string Name { get; private set; } + public int Components { get; private set; } + public int BitsPerPixel { get; private set; } - public bool CanConvertTo(PixelFormat other) + public bool CanConvertTo(PixelFormat other) + { + return InputConversionSupported && other.OutputConversionSupported; + } + + internal static bool TryParse(string line, out PixelFormat fmt) + { + var match = _formatRegex.Match(line); + if (!match.Success) { - return InputConversionSupported && other.OutputConversionSupported; + fmt = null!; + return false; } - internal PixelFormat(string name) + fmt = new PixelFormat(match.Groups[6].Value); + fmt.InputConversionSupported = match.Groups[1].Value != "."; + fmt.OutputConversionSupported = match.Groups[2].Value != "."; + fmt.HardwareAccelerationSupported = match.Groups[3].Value != "."; + fmt.IsPaletted = match.Groups[4].Value != "."; + fmt.IsBitstream = match.Groups[5].Value != "."; + if (!int.TryParse(match.Groups[7].Value, out var nbComponents)) { - Name = name; + return false; } - internal static bool TryParse(string line, out PixelFormat fmt) + fmt.Components = nbComponents; + if (!int.TryParse(match.Groups[8].Value, out var bpp)) { - var match = _formatRegex.Match(line); - if (!match.Success) - { - fmt = null!; - return false; - } - - fmt = new PixelFormat(match.Groups[6].Value); - fmt.InputConversionSupported = match.Groups[1].Value != "."; - fmt.OutputConversionSupported = match.Groups[2].Value != "."; - fmt.HardwareAccelerationSupported = match.Groups[3].Value != "."; - fmt.IsPaletted = match.Groups[4].Value != "."; - fmt.IsBitstream = match.Groups[5].Value != "."; - if (!int.TryParse(match.Groups[7].Value, out var nbComponents)) - { - return false; - } - - fmt.Components = nbComponents; - if (!int.TryParse(match.Groups[8].Value, out var bpp)) - { - return false; - } - - fmt.BitsPerPixel = bpp; - - return true; + return false; } + + fmt.BitsPerPixel = bpp; + + return true; } } diff --git a/FFMpegCore/FFMpeg/Enums/Speed.cs b/FFMpegCore/FFMpeg/Enums/Speed.cs index 8400b56..3aaf0cd 100644 --- a/FFMpegCore/FFMpeg/Enums/Speed.cs +++ b/FFMpegCore/FFMpeg/Enums/Speed.cs @@ -1,15 +1,14 @@ -namespace FFMpegCore.Enums +namespace FFMpegCore.Enums; + +public enum Speed { - public enum Speed - { - VerySlow, - Slower, - Slow, - Medium, - Fast, - Faster, - VeryFast, - SuperFast, - UltraFast - } + VerySlow, + Slower, + Slow, + Medium, + Fast, + Faster, + VeryFast, + SuperFast, + UltraFast } diff --git a/FFMpegCore/FFMpeg/Enums/Transposition.cs b/FFMpegCore/FFMpeg/Enums/Transposition.cs index 1e47bbb..ecbeed5 100644 --- a/FFMpegCore/FFMpeg/Enums/Transposition.cs +++ b/FFMpegCore/FFMpeg/Enums/Transposition.cs @@ -1,10 +1,9 @@ -namespace FFMpegCore.Enums +namespace FFMpegCore.Enums; + +public enum Transposition { - public enum Transposition - { - CounterClockwise90VerticalFlip = 0, - Clockwise90 = 1, - CounterClockwise90 = 2, - Clockwise90VerticalFlip = 3 - } + CounterClockwise90VerticalFlip = 0, + Clockwise90 = 1, + CounterClockwise90 = 2, + Clockwise90VerticalFlip = 3 } diff --git a/FFMpegCore/FFMpeg/Enums/VideoSize.cs b/FFMpegCore/FFMpeg/Enums/VideoSize.cs index 29e203b..7fb98dc 100644 --- a/FFMpegCore/FFMpeg/Enums/VideoSize.cs +++ b/FFMpegCore/FFMpeg/Enums/VideoSize.cs @@ -1,11 +1,10 @@ -namespace FFMpegCore.Enums +namespace FFMpegCore.Enums; + +public enum VideoSize { - public enum VideoSize - { - FullHd = 1080, - Hd = 720, - Ed = 480, - Ld = 360, - Original = -1 - } + FullHd = 1080, + Hd = 720, + Ed = 480, + Ld = 360, + Original = -1 } diff --git a/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs b/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs index e92ee56..dd9dbc6 100644 --- a/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs +++ b/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs @@ -1,58 +1,60 @@ -namespace FFMpegCore.Exceptions +namespace FFMpegCore.Exceptions; + +public enum FFMpegExceptionType { - public enum FFMpegExceptionType + Conversion, + File, + Operation, + Process +} + +public class FFMpegException : Exception +{ + public FFMpegException(FFMpegExceptionType type, string message, Exception? innerException = null, string ffMpegErrorOutput = "") + : base(message, innerException) { - Conversion, - File, - Operation, - Process + FFMpegErrorOutput = ffMpegErrorOutput; + Type = type; } - public class FFMpegException : Exception + public FFMpegException(FFMpegExceptionType type, string message, string ffMpegErrorOutput = "") + : base(message) { - public FFMpegException(FFMpegExceptionType type, string message, Exception? innerException = null, string ffMpegErrorOutput = "") - : base(message, innerException) - { - FFMpegErrorOutput = ffMpegErrorOutput; - Type = type; - } - public FFMpegException(FFMpegExceptionType type, string message, string ffMpegErrorOutput = "") - : base(message) - { - FFMpegErrorOutput = ffMpegErrorOutput; - Type = type; - } - public FFMpegException(FFMpegExceptionType type, string message) - : base(message) - { - FFMpegErrorOutput = string.Empty; - Type = type; - } - - public FFMpegExceptionType Type { get; } - public string FFMpegErrorOutput { get; } - } - public class FFOptionsException : Exception - { - public FFOptionsException(string message, Exception? innerException = null) - : base(message, innerException) - { - } + FFMpegErrorOutput = ffMpegErrorOutput; + Type = type; } - public class FFMpegArgumentException : Exception + public FFMpegException(FFMpegExceptionType type, string message) + : base(message) { - public FFMpegArgumentException(string? message = null, Exception? innerException = null) - : base(message, innerException) - { - } + FFMpegErrorOutput = string.Empty; + Type = type; } - public class FFMpegStreamFormatException : FFMpegException + public FFMpegExceptionType Type { get; } + public string FFMpegErrorOutput { get; } +} + +public class FFOptionsException : Exception +{ + public FFOptionsException(string message, Exception? innerException = null) + : base(message, innerException) + { + } +} + +public class FFMpegArgumentException : Exception +{ + public FFMpegArgumentException(string? message = null, Exception? innerException = null) + : base(message, innerException) + { + } +} + +public class FFMpegStreamFormatException : FFMpegException +{ + public FFMpegStreamFormatException(FFMpegExceptionType type, string message, Exception? innerException = null) + : base(type, message, innerException) { - public FFMpegStreamFormatException(FFMpegExceptionType type, string message, Exception? innerException = null) - : base(type, message, innerException) - { - } } } diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index 820d9fb..7076a49 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -4,643 +4,666 @@ using FFMpegCore.Exceptions; using FFMpegCore.Helpers; using Instances; -namespace FFMpegCore +namespace FFMpegCore; + +public static class FFMpeg { - public static class FFMpeg + /// + /// Saves a 'png' thumbnail from the input video to drive + /// + /// Source video analysis + /// Output video file path + /// Seek position where the thumbnail should be taken. + /// Thumbnail size. If width or height equal 0, the other will be computed automatically. + /// Selected video stream index. + /// Input file index + /// Bitmap with the requested snapshot. + public static bool Snapshot(string input, string output, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0) { - /// - /// Saves a 'png' thumbnail from the input video to drive - /// - /// Source video analysis - /// Output video file path - /// Seek position where the thumbnail should be taken. - /// Thumbnail size. If width or height equal 0, the other will be computed automatically. - /// Selected video stream index. - /// Input file index - /// Bitmap with the requested snapshot. - public static bool Snapshot(string input, string output, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0) + CheckSnapshotOutputExtension(output, FileExtension.Image.All); + + var source = FFProbe.Analyse(input); + + return SnapshotProcess(input, output, source, size, captureTime, streamIndex, inputFileIndex) + .ProcessSynchronously(); + } + + /// + /// Saves a 'png' thumbnail from the input video to drive + /// + /// Source video analysis + /// Output video file path + /// Seek position where the thumbnail should be taken. + /// Thumbnail size. If width or height equal 0, the other will be computed automatically. + /// Selected video stream index. + /// Input file index + /// Bitmap with the requested snapshot. + public static async Task SnapshotAsync(string input, string output, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, + int inputFileIndex = 0) + { + CheckSnapshotOutputExtension(output, FileExtension.Image.All); + + var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false); + + return await SnapshotProcess(input, output, source, size, captureTime, streamIndex, inputFileIndex) + .ProcessAsynchronously(); + } + + public static bool GifSnapshot(string input, string output, Size? size = null, TimeSpan? captureTime = null, TimeSpan? duration = null, + int? streamIndex = null) + { + CheckSnapshotOutputExtension(output, [FileExtension.Gif]); + + var source = FFProbe.Analyse(input); + + return GifSnapshotProcess(input, output, source, size, captureTime, duration, streamIndex) + .ProcessSynchronously(); + } + + public static async Task GifSnapshotAsync(string input, string output, Size? size = null, TimeSpan? captureTime = null, TimeSpan? duration = null, + int? streamIndex = null) + { + CheckSnapshotOutputExtension(output, [FileExtension.Gif]); + + var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false); + + return await GifSnapshotProcess(input, output, source, size, captureTime, duration, streamIndex) + .ProcessAsynchronously(); + } + + private static FFMpegArgumentProcessor SnapshotProcess(string input, string output, IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null, + int? streamIndex = null, int inputFileIndex = 0) + { + var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, output, source, size, captureTime, streamIndex, inputFileIndex); + + return arguments.OutputToFile(output, true, outputOptions); + } + + private static FFMpegArgumentProcessor GifSnapshotProcess(string input, string output, IMediaAnalysis source, Size? size = null, + TimeSpan? captureTime = null, TimeSpan? duration = null, int? streamIndex = null) + { + var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildGifSnapshotArguments(input, source, size, captureTime, duration, streamIndex); + + return arguments.OutputToFile(output, true, outputOptions); + } + + private static void CheckSnapshotOutputExtension(string output, List extensions) + { + if (!extensions.Contains(Path.GetExtension(output).ToLower())) { - if (Path.GetExtension(output) != FileExtension.Png) - { - output = Path.Combine(Path.GetDirectoryName(output), Path.GetFileNameWithoutExtension(output) + FileExtension.Png); - } - - var source = FFProbe.Analyse(input); - var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); - - return arguments - .OutputToFile(output, true, outputOptions) - .ProcessSynchronously(); + throw new ArgumentException( + $"Invalid snapshot output extension: {output}, needed: {string.Join(",", FileExtension.Image.All)}"); } - /// - /// Saves a 'png' thumbnail from the input video to drive - /// - /// Source video analysis - /// Output video file path - /// Seek position where the thumbnail should be taken. - /// Thumbnail size. If width or height equal 0, the other will be computed automatically. - /// Selected video stream index. - /// Input file index - /// Bitmap with the requested snapshot. - public static async Task SnapshotAsync(string input, string output, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0) + } + + /// + /// Converts an image sequence to a video. + /// + /// Output video file. + /// FPS + /// Image sequence collection + /// Output video information. + public static bool JoinImageSequence(string output, double frameRate = 30, params string[] images) + { + var fileExtensions = images.Select(Path.GetExtension).Distinct().ToArray(); + if (fileExtensions.Length != 1) { - if (Path.GetExtension(output) != FileExtension.Png) - { - output = Path.Combine(Path.GetDirectoryName(output), Path.GetFileNameWithoutExtension(output) + FileExtension.Png); - } - - var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false); - var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); - - return await arguments - .OutputToFile(output, true, outputOptions) - .ProcessAsynchronously(); + throw new ArgumentException("All images must have the same extension", nameof(images)); } - public static bool GifSnapshot(string input, string output, Size? size = null, TimeSpan? captureTime = null, TimeSpan? duration = null, int? streamIndex = null) + var fileExtension = fileExtensions[0].ToLowerInvariant(); + int? width = null, height = null; + + var tempFolderName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempFolderName); + + try { - if (Path.GetExtension(output)?.ToLower() != FileExtension.Gif) + var index = 0; + foreach (var imagePath in images) { - output = Path.Combine(Path.GetDirectoryName(output), Path.GetFileNameWithoutExtension(output) + FileExtension.Gif); + var analysis = FFProbe.Analyse(imagePath); + FFMpegHelper.ConversionSizeExceptionCheck(analysis.PrimaryVideoStream!.Width, analysis.PrimaryVideoStream!.Height); + width ??= analysis.PrimaryVideoStream.Width; + height ??= analysis.PrimaryVideoStream.Height; + + var destinationPath = Path.Combine(tempFolderName, $"{index++.ToString().PadLeft(9, '0')}{fileExtension}"); + File.Copy(imagePath, destinationPath); } - var source = FFProbe.Analyse(input); - var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildGifSnapshotArguments(input, source, size, captureTime, duration, streamIndex); - - return arguments - .OutputToFile(output, true, outputOptions) - .ProcessSynchronously(); - } - - public static async Task GifSnapshotAsync(string input, string output, Size? size = null, TimeSpan? captureTime = null, TimeSpan? duration = null, int? streamIndex = null) - { - if (Path.GetExtension(output)?.ToLower() != FileExtension.Gif) - { - output = Path.Combine(Path.GetDirectoryName(output), Path.GetFileNameWithoutExtension(output) + FileExtension.Gif); - } - - var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false); - var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildGifSnapshotArguments(input, source, size, captureTime, duration, streamIndex); - - return await arguments - .OutputToFile(output, true, outputOptions) - .ProcessAsynchronously(); - } - - /// - /// Converts an image sequence to a video. - /// - /// Output video file. - /// FPS - /// Image sequence collection - /// Output video information. - public static bool JoinImageSequence(string output, double frameRate = 30, params string[] images) - { - var fileExtensions = images.Select(Path.GetExtension).Distinct().ToArray(); - if (fileExtensions.Length != 1) - { - throw new ArgumentException("All images must have the same extension", nameof(images)); - } - - var fileExtension = fileExtensions[0].ToLowerInvariant(); - int? width = null, height = null; - - var tempFolderName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, Guid.NewGuid().ToString()); - Directory.CreateDirectory(tempFolderName); - - try - { - var index = 0; - foreach (var imagePath in images) - { - var analysis = FFProbe.Analyse(imagePath); - FFMpegHelper.ConversionSizeExceptionCheck(analysis.PrimaryVideoStream!.Width, analysis.PrimaryVideoStream!.Height); - width ??= analysis.PrimaryVideoStream.Width; - height ??= analysis.PrimaryVideoStream.Height; - - var destinationPath = Path.Combine(tempFolderName, $"{index++.ToString().PadLeft(9, '0')}{fileExtension}"); - File.Copy(imagePath, destinationPath); - } - - return FFMpegArguments - .FromFileInput(Path.Combine(tempFolderName, $"%09d{fileExtension}"), false) - .OutputToFile(output, true, options => options - .ForcePixelFormat("yuv420p") - .Resize(width!.Value, height!.Value) - .WithFramerate(frameRate)) - .ProcessSynchronously(); - } - finally - { - Directory.Delete(tempFolderName, true); - } - } - - /// - /// Adds a poster image to an audio file. - /// - /// Source image file. - /// Source audio file. - /// Output video file. - /// - public static bool PosterWithAudio(string image, string audio, string output) - { - FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.Mp4); - var analysis = FFProbe.Analyse(image); - FFMpegHelper.ConversionSizeExceptionCheck(analysis.PrimaryVideoStream!.Width, analysis.PrimaryVideoStream!.Height); - return FFMpegArguments - .FromFileInput(image, false, options => options - .Loop(1) - .ForceFormat("image2")) - .AddFileInput(audio) + .FromFileInput(Path.Combine(tempFolderName, $"%09d{fileExtension}"), false) .OutputToFile(output, true, options => options .ForcePixelFormat("yuv420p") + .Resize(width!.Value, height!.Value) + .WithFramerate(frameRate)) + .ProcessSynchronously(); + } + finally + { + Directory.Delete(tempFolderName, true); + } + } + + /// + /// Adds a poster image to an audio file. + /// + /// Source image file. + /// Source audio file. + /// Output video file. + /// + public static bool PosterWithAudio(string image, string audio, string output) + { + FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.Mp4); + var analysis = FFProbe.Analyse(image); + FFMpegHelper.ConversionSizeExceptionCheck(analysis.PrimaryVideoStream!.Width, analysis.PrimaryVideoStream!.Height); + + return FFMpegArguments + .FromFileInput(image, false, options => options + .Loop(1) + .ForceFormat("image2")) + .AddFileInput(audio) + .OutputToFile(output, true, options => options + .ForcePixelFormat("yuv420p") + .WithVideoCodec(VideoCodec.LibX264) + .WithConstantRateFactor(21) + .WithAudioBitrate(AudioQuality.Normal) + .UsingShortest()) + .ProcessSynchronously(); + } + + /// + /// Convert a video do a different format. + /// + /// Input video source. + /// Output information. + /// Target conversion video format. + /// Conversion target speed/quality (faster speed = lower quality). + /// Video size. + /// Conversion target audio quality. + /// Is encoding multithreaded. + /// Output video information. + public static bool Convert( + string input, + string output, + ContainerFormat format, + Speed speed = Speed.SuperFast, + VideoSize size = VideoSize.Original, + AudioQuality audioQuality = AudioQuality.Normal, + bool multithreaded = false) + { + FFMpegHelper.ExtensionExceptionCheck(output, format.Extension); + var source = FFProbe.Analyse(input); + FFMpegHelper.ConversionSizeExceptionCheck(source); + + var scale = VideoSize.Original == size ? 1 : (double)source.PrimaryVideoStream!.Height / (int)size; + var outputSize = new Size((int)(source.PrimaryVideoStream!.Width / scale), (int)(source.PrimaryVideoStream.Height / scale)); + + if (outputSize.Width % 2 != 0) + { + outputSize.Width += 1; + } + + return format.Name switch + { + "mp4" => FFMpegArguments + .FromFileInput(input) + .OutputToFile(output, true, options => options + .UsingMultithreading(multithreaded) .WithVideoCodec(VideoCodec.LibX264) - .WithConstantRateFactor(21) - .WithAudioBitrate(AudioQuality.Normal) - .UsingShortest()) - .ProcessSynchronously(); - } - - /// - /// Convert a video do a different format. - /// - /// Input video source. - /// Output information. - /// Target conversion video format. - /// Conversion target speed/quality (faster speed = lower quality). - /// Video size. - /// Conversion target audio quality. - /// Is encoding multithreaded. - /// Output video information. - public static bool Convert( - string input, - string output, - ContainerFormat format, - Speed speed = Speed.SuperFast, - VideoSize size = VideoSize.Original, - AudioQuality audioQuality = AudioQuality.Normal, - bool multithreaded = false) - { - FFMpegHelper.ExtensionExceptionCheck(output, format.Extension); - var source = FFProbe.Analyse(input); - FFMpegHelper.ConversionSizeExceptionCheck(source); - - var scale = VideoSize.Original == size ? 1 : (double)source.PrimaryVideoStream!.Height / (int)size; - var outputSize = new Size((int)(source.PrimaryVideoStream!.Width / scale), (int)(source.PrimaryVideoStream.Height / scale)); - - if (outputSize.Width % 2 != 0) - { - outputSize.Width += 1; - } - - return format.Name switch - { - "mp4" => FFMpegArguments - .FromFileInput(input) - .OutputToFile(output, true, options => options - .UsingMultithreading(multithreaded) - .WithVideoCodec(VideoCodec.LibX264) - .WithVideoBitrate(2400) - .WithVideoFilters(filterOptions => filterOptions - .Scale(outputSize)) - .WithSpeedPreset(speed) - .WithAudioCodec(AudioCodec.Aac) - .WithAudioBitrate(audioQuality)) - .ProcessSynchronously(), - "ogv" => FFMpegArguments - .FromFileInput(input) - .OutputToFile(output, true, options => options - .UsingMultithreading(multithreaded) - .WithVideoCodec(VideoCodec.LibTheora) - .WithVideoBitrate(2400) - .WithVideoFilters(filterOptions => filterOptions - .Scale(outputSize)) - .WithSpeedPreset(speed) - .WithAudioCodec(AudioCodec.LibVorbis) - .WithAudioBitrate(audioQuality)) - .ProcessSynchronously(), - "mpegts" => FFMpegArguments - .FromFileInput(input) - .OutputToFile(output, true, options => options - .CopyChannel() - .WithBitStreamFilter(Channel.Video, Filter.H264_Mp4ToAnnexB) - .ForceFormat(VideoType.Ts)) - .ProcessSynchronously(), - "webm" => FFMpegArguments - .FromFileInput(input) - .OutputToFile(output, true, options => options - .UsingMultithreading(multithreaded) - .WithVideoCodec(VideoCodec.LibVpx) - .WithVideoBitrate(2400) - .WithVideoFilters(filterOptions => filterOptions - .Scale(outputSize)) - .WithSpeedPreset(speed) - .WithAudioCodec(AudioCodec.LibVorbis) - .WithAudioBitrate(audioQuality)) - .ProcessSynchronously(), - _ => throw new ArgumentOutOfRangeException(nameof(format)) - }; - } - - /// - /// Joins a list of video files. - /// - /// Output video file. - /// List of vides that need to be joined together. - /// Output video information. - public static bool Join(string output, params string[] videos) - { - var temporaryVideoParts = videos.Select(videoPath => - { - var video = FFProbe.Analyse(videoPath); - FFMpegHelper.ConversionSizeExceptionCheck(video); - var destinationPath = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, $"{Path.GetFileNameWithoutExtension(videoPath)}{FileExtension.Ts}"); - Directory.CreateDirectory(GlobalFFOptions.Current.TemporaryFilesFolder); - Convert(videoPath, destinationPath, VideoType.Ts); - return destinationPath; - }).ToArray(); - - try - { - return FFMpegArguments - .FromConcatInput(temporaryVideoParts) - .OutputToFile(output, true, options => options - .CopyChannel() - .WithBitStreamFilter(Channel.Audio, Filter.Aac_AdtstoAsc)) - .ProcessSynchronously(); - } - finally - { - Cleanup(temporaryVideoParts); - } - } - - private static FFMpegArgumentProcessor BaseSubVideo(string input, string output, TimeSpan startTime, TimeSpan endTime) - { - if (Path.GetExtension(input) != Path.GetExtension(output)) - { - output = Path.Combine(Path.GetDirectoryName(output), Path.GetFileNameWithoutExtension(output), Path.GetExtension(input)); - } - - return FFMpegArguments - .FromFileInput(input, true, options => options.Seek(startTime).EndSeek(endTime)) - .OutputToFile(output, true, options => options.CopyChannel()); - } - - /// - /// Creates a new video starting and ending at the specified times - /// - /// Input video file. - /// Output video file. - /// The start time of when the sub video needs to start - /// The end time of where the sub video needs to end - /// Output video information. - public static bool SubVideo(string input, string output, TimeSpan startTime, TimeSpan endTime) - { - return BaseSubVideo(input, output, startTime, endTime) - .ProcessSynchronously(); - } - - /// - /// Creates a new video starting and ending at the specified times - /// - /// Input video file. - /// Output video file. - /// The start time of when the sub video needs to start - /// The end time of where the sub video needs to end - /// Output video information. - public static async Task SubVideoAsync(string input, string output, TimeSpan startTime, TimeSpan endTime) - { - return await BaseSubVideo(input, output, startTime, endTime) - .ProcessAsynchronously(); - } - - /// - /// Records M3U8 streams to the specified output. - /// - /// URI to pointing towards stream. - /// Output file - /// Success state. - public static bool SaveM3U8Stream(Uri uri, string output) - { - FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.Mp4); - - if (uri.Scheme != "http" && uri.Scheme != "https") - { - throw new ArgumentException($"Uri: {uri.AbsoluteUri}, does not point to a valid http(s) stream."); - } - - return FFMpegArguments - .FromUrlInput(uri, options => - { - options.WithCopyCodec(); - }) - .OutputToFile(output) - .ProcessSynchronously(); - } - - /// - /// Strips a video file of audio. - /// - /// Input video file. - /// Output video file. - /// - public static bool Mute(string input, string output) - { - var source = FFProbe.Analyse(input); - FFMpegHelper.ConversionSizeExceptionCheck(source); - // FFMpegHelper.ExtensionExceptionCheck(output, source.Extension); - - return FFMpegArguments + .WithVideoBitrate(2400) + .WithVideoFilters(filterOptions => filterOptions + .Scale(outputSize)) + .WithSpeedPreset(speed) + .WithAudioCodec(AudioCodec.Aac) + .WithAudioBitrate(audioQuality)) + .ProcessSynchronously(), + "ogv" => FFMpegArguments .FromFileInput(input) .OutputToFile(output, true, options => options - .CopyChannel(Channel.Video) - .DisableChannel(Channel.Audio)) - .ProcessSynchronously(); - } - - /// - /// Saves audio from a specific video file to disk. - /// - /// Source video file. - /// Output audio file. - /// Success state. - public static bool ExtractAudio(string input, string output) - { - FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.Mp3); - - return FFMpegArguments + .UsingMultithreading(multithreaded) + .WithVideoCodec(VideoCodec.LibTheora) + .WithVideoBitrate(2400) + .WithVideoFilters(filterOptions => filterOptions + .Scale(outputSize)) + .WithSpeedPreset(speed) + .WithAudioCodec(AudioCodec.LibVorbis) + .WithAudioBitrate(audioQuality)) + .ProcessSynchronously(), + "mpegts" => FFMpegArguments .FromFileInput(input) - .OutputToFile(output, true, options => options - .DisableChannel(Channel.Video)) - .ProcessSynchronously(); - } - - /// - /// Adds audio to a video file. - /// - /// Source video file. - /// Source audio file. - /// Output video file. - /// Indicates if the encoding should stop at the shortest input file. - /// Success state - public static bool ReplaceAudio(string input, string inputAudio, string output, bool stopAtShortest = false) - { - var source = FFProbe.Analyse(input); - FFMpegHelper.ConversionSizeExceptionCheck(source); - // FFMpegHelper.ExtensionExceptionCheck(output, source.Format.); - - return FFMpegArguments - .FromFileInput(input) - .AddFileInput(inputAudio) .OutputToFile(output, true, options => options .CopyChannel() - .WithAudioCodec(AudioCodec.Aac) - .WithAudioBitrate(AudioQuality.Good) - .UsingShortest(stopAtShortest)) + .WithBitStreamFilter(Channel.Video, Filter.H264_Mp4ToAnnexB) + .ForceFormat(VideoType.Ts)) + .ProcessSynchronously(), + "webm" => FFMpegArguments + .FromFileInput(input) + .OutputToFile(output, true, options => options + .UsingMultithreading(multithreaded) + .WithVideoCodec(VideoCodec.LibVpx) + .WithVideoBitrate(2400) + .WithVideoFilters(filterOptions => filterOptions + .Scale(outputSize)) + .WithSpeedPreset(speed) + .WithAudioCodec(AudioCodec.LibVorbis) + .WithAudioBitrate(audioQuality)) + .ProcessSynchronously(), + _ => throw new ArgumentOutOfRangeException(nameof(format)) + }; + } + + /// + /// Joins a list of video files. + /// + /// Output video file. + /// List of vides that need to be joined together. + /// Output video information. + public static bool Join(string output, params string[] videos) + { + var temporaryVideoParts = videos.Select(videoPath => + { + var video = FFProbe.Analyse(videoPath); + FFMpegHelper.ConversionSizeExceptionCheck(video); + var destinationPath = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, + $"{Path.GetFileNameWithoutExtension(videoPath)}{FileExtension.Ts}"); + Directory.CreateDirectory(GlobalFFOptions.Current.TemporaryFilesFolder); + Convert(videoPath, destinationPath, VideoType.Ts); + return destinationPath; + }).ToArray(); + + try + { + return FFMpegArguments + .FromConcatInput(temporaryVideoParts) + .OutputToFile(output, true, options => options + .CopyChannel() + .WithBitStreamFilter(Channel.Audio, Filter.Aac_AdtstoAsc)) .ProcessSynchronously(); } - - #region PixelFormats - internal static IReadOnlyList GetPixelFormatsInternal() + finally { - FFMpegHelper.RootExceptionCheck(); + Cleanup(temporaryVideoParts); + } + } - var list = new List(); - var processArguments = new ProcessArguments(GlobalFFOptions.GetFFMpegBinaryPath(), "-pix_fmts"); - processArguments.OutputDataReceived += (e, data) => + private static FFMpegArgumentProcessor BaseSubVideo(string input, string output, TimeSpan startTime, TimeSpan endTime) + { + if (Path.GetExtension(input) != Path.GetExtension(output)) + { + output = Path.Combine(Path.GetDirectoryName(output), Path.GetFileNameWithoutExtension(output), Path.GetExtension(input)); + } + + return FFMpegArguments + .FromFileInput(input, true, options => options.Seek(startTime).EndSeek(endTime)) + .OutputToFile(output, true, options => options.CopyChannel()); + } + + /// + /// Creates a new video starting and ending at the specified times + /// + /// Input video file. + /// Output video file. + /// The start time of when the sub video needs to start + /// The end time of where the sub video needs to end + /// Output video information. + public static bool SubVideo(string input, string output, TimeSpan startTime, TimeSpan endTime) + { + return BaseSubVideo(input, output, startTime, endTime) + .ProcessSynchronously(); + } + + /// + /// Creates a new video starting and ending at the specified times + /// + /// Input video file. + /// Output video file. + /// The start time of when the sub video needs to start + /// The end time of where the sub video needs to end + /// Output video information. + public static async Task SubVideoAsync(string input, string output, TimeSpan startTime, TimeSpan endTime) + { + return await BaseSubVideo(input, output, startTime, endTime) + .ProcessAsynchronously(); + } + + /// + /// Records M3U8 streams to the specified output. + /// + /// URI to pointing towards stream. + /// Output file + /// Success state. + public static bool SaveM3U8Stream(Uri uri, string output) + { + FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.Mp4); + + if (uri.Scheme != "http" && uri.Scheme != "https") + { + throw new ArgumentException($"Uri: {uri.AbsoluteUri}, does not point to a valid http(s) stream."); + } + + return FFMpegArguments + .FromUrlInput(uri, options => { - if (PixelFormat.TryParse(data, out var format)) + options.WithCopyCodec(); + }) + .OutputToFile(output) + .ProcessSynchronously(); + } + + /// + /// Strips a video file of audio. + /// + /// Input video file. + /// Output video file. + /// + public static bool Mute(string input, string output) + { + var source = FFProbe.Analyse(input); + FFMpegHelper.ConversionSizeExceptionCheck(source); + // FFMpegHelper.ExtensionExceptionCheck(output, source.Extension); + + return FFMpegArguments + .FromFileInput(input) + .OutputToFile(output, true, options => options + .CopyChannel(Channel.Video) + .DisableChannel(Channel.Audio)) + .ProcessSynchronously(); + } + + /// + /// Saves audio from a specific video file to disk. + /// + /// Source video file. + /// Output audio file. + /// Success state. + public static bool ExtractAudio(string input, string output) + { + FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.Mp3); + + return FFMpegArguments + .FromFileInput(input) + .OutputToFile(output, true, options => options + .DisableChannel(Channel.Video)) + .ProcessSynchronously(); + } + + /// + /// Adds audio to a video file. + /// + /// Source video file. + /// Source audio file. + /// Output video file. + /// Indicates if the encoding should stop at the shortest input file. + /// Success state + public static bool ReplaceAudio(string input, string inputAudio, string output, bool stopAtShortest = false) + { + var source = FFProbe.Analyse(input); + FFMpegHelper.ConversionSizeExceptionCheck(source); + // FFMpegHelper.ExtensionExceptionCheck(output, source.Format.); + + return FFMpegArguments + .FromFileInput(input) + .AddFileInput(inputAudio) + .OutputToFile(output, true, options => options + .CopyChannel() + .WithAudioCodec(AudioCodec.Aac) + .WithAudioBitrate(AudioQuality.Good) + .UsingShortest(stopAtShortest)) + .ProcessSynchronously(); + } + + private static void Cleanup(IEnumerable pathList) + { + foreach (var path in pathList) + { + if (File.Exists(path)) + { + File.Delete(path); + } + } + } + + #region PixelFormats + + internal static IReadOnlyList GetPixelFormatsInternal() + { + FFMpegHelper.RootExceptionCheck(); + + var list = new List(); + var processArguments = new ProcessArguments(GlobalFFOptions.GetFFMpegBinaryPath(), "-pix_fmts"); + processArguments.OutputDataReceived += (e, data) => + { + if (PixelFormat.TryParse(data, out var format)) + { + list.Add(format); + } + }; + + var result = processArguments.StartAndWaitForExit(); + if (result.ExitCode != 0) + { + throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\r\n", result.OutputData)); + } + + return list.AsReadOnly(); + } + + public static IReadOnlyList GetPixelFormats() + { + if (!GlobalFFOptions.Current.UseCache) + { + return GetPixelFormatsInternal(); + } + + return FFMpegCache.PixelFormats.Values.ToList().AsReadOnly(); + } + + public static bool TryGetPixelFormat(string name, out PixelFormat format) + { + if (!GlobalFFOptions.Current.UseCache) + { + format = GetPixelFormatsInternal().FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim()); + return format != null; + } + + return FFMpegCache.PixelFormats.TryGetValue(name, out format); + } + + public static PixelFormat GetPixelFormat(string name) + { + if (TryGetPixelFormat(name, out var fmt)) + { + return fmt; + } + + throw new FFMpegException(FFMpegExceptionType.Operation, $"Pixel format \"{name}\" not supported"); + } + + #endregion + + #region Codecs + + private static void ParsePartOfCodecs(Dictionary codecs, string arguments, Func parser) + { + FFMpegHelper.RootExceptionCheck(); + + var processArguments = new ProcessArguments(GlobalFFOptions.GetFFMpegBinaryPath(), arguments); + processArguments.OutputDataReceived += (e, data) => + { + var codec = parser(data); + if (codec != null) + { + if (codecs.TryGetValue(codec.Name, out var parentCodec)) { - list.Add(format); + parentCodec.Merge(codec); } - }; - - var result = processArguments.StartAndWaitForExit(); - if (result.ExitCode != 0) - { - throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\r\n", result.OutputData)); - } - - return list.AsReadOnly(); - } - - public static IReadOnlyList GetPixelFormats() - { - if (!GlobalFFOptions.Current.UseCache) - { - return GetPixelFormatsInternal(); - } - - return FFMpegCache.PixelFormats.Values.ToList().AsReadOnly(); - } - - public static bool TryGetPixelFormat(string name, out PixelFormat format) - { - if (!GlobalFFOptions.Current.UseCache) - { - format = GetPixelFormatsInternal().FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim()); - return format != null; - } - else - { - return FFMpegCache.PixelFormats.TryGetValue(name, out format); - } - } - - public static PixelFormat GetPixelFormat(string name) - { - if (TryGetPixelFormat(name, out var fmt)) - { - return fmt; - } - - throw new FFMpegException(FFMpegExceptionType.Operation, $"Pixel format \"{name}\" not supported"); - } - #endregion - - #region Codecs - - private static void ParsePartOfCodecs(Dictionary codecs, string arguments, Func parser) - { - FFMpegHelper.RootExceptionCheck(); - - var processArguments = new ProcessArguments(GlobalFFOptions.GetFFMpegBinaryPath(), arguments); - processArguments.OutputDataReceived += (e, data) => - { - var codec = parser(data); - if (codec != null) + else { - if (codecs.TryGetValue(codec.Name, out var parentCodec)) - { - parentCodec.Merge(codec); - } - else - { - codecs.Add(codec.Name, codec); - } + codecs.Add(codec.Name, codec); } - }; - - var result = processArguments.StartAndWaitForExit(); - if (result.ExitCode != 0) - { - throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\r\n", result.OutputData)); } - } + }; - internal static Dictionary GetCodecsInternal() + var result = processArguments.StartAndWaitForExit(); + if (result.ExitCode != 0) { - var res = new Dictionary(); - ParsePartOfCodecs(res, "-codecs", (s) => - { - if (Codec.TryParseFromCodecs(s, out var codec)) - { - return codec; - } - - return null; - }); - ParsePartOfCodecs(res, "-encoders", (s) => - { - if (Codec.TryParseFromEncodersDecoders(s, out var codec, true)) - { - return codec; - } - - return null; - }); - ParsePartOfCodecs(res, "-decoders", (s) => - { - if (Codec.TryParseFromEncodersDecoders(s, out var codec, false)) - { - return codec; - } - - return null; - }); - - return res; + throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\r\n", result.OutputData)); } + } - public static IReadOnlyList GetCodecs() + internal static Dictionary GetCodecsInternal() + { + var res = new Dictionary(); + ParsePartOfCodecs(res, "-codecs", s => { - if (!GlobalFFOptions.Current.UseCache) - { - return GetCodecsInternal().Values.ToList().AsReadOnly(); - } - - return FFMpegCache.Codecs.Values.ToList().AsReadOnly(); - } - - public static IReadOnlyList GetCodecs(CodecType type) - { - if (!GlobalFFOptions.Current.UseCache) - { - return GetCodecsInternal().Values.Where(x => x.Type == type).ToList().AsReadOnly(); - } - - return FFMpegCache.Codecs.Values.Where(x => x.Type == type).ToList().AsReadOnly(); - } - - public static IReadOnlyList GetVideoCodecs() => GetCodecs(CodecType.Video); - public static IReadOnlyList GetAudioCodecs() => GetCodecs(CodecType.Audio); - public static IReadOnlyList GetSubtitleCodecs() => GetCodecs(CodecType.Subtitle); - public static IReadOnlyList GetDataCodecs() => GetCodecs(CodecType.Data); - - public static bool TryGetCodec(string name, out Codec codec) - { - if (!GlobalFFOptions.Current.UseCache) - { - codec = GetCodecsInternal().Values.FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim()); - return codec != null; - } - else - { - return FFMpegCache.Codecs.TryGetValue(name, out codec); - } - } - - public static Codec GetCodec(string name) - { - if (TryGetCodec(name, out var codec) && codec != null) + if (Codec.TryParseFromCodecs(s, out var codec)) { return codec; } - throw new FFMpegException(FFMpegExceptionType.Operation, $"Codec \"{name}\" not supported"); - } - #endregion - - #region ContainerFormats - internal static IReadOnlyList GetContainersFormatsInternal() + return null; + }); + ParsePartOfCodecs(res, "-encoders", s => { - FFMpegHelper.RootExceptionCheck(); - - var list = new List(); - var instance = new ProcessArguments(GlobalFFOptions.GetFFMpegBinaryPath(), "-formats"); - instance.OutputDataReceived += (e, data) => + if (Codec.TryParseFromEncodersDecoders(s, out var codec, true)) { - if (ContainerFormat.TryParse(data, out var fmt)) - { - list.Add(fmt); - } - }; - - var result = instance.StartAndWaitForExit(); - if (result.ExitCode != 0) - { - throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\r\n", result.OutputData)); + return codec; } - return list.AsReadOnly(); - } - - public static IReadOnlyList GetContainerFormats() + return null; + }); + ParsePartOfCodecs(res, "-decoders", s => { - if (!GlobalFFOptions.Current.UseCache) + if (Codec.TryParseFromEncodersDecoders(s, out var codec, false)) { - return GetContainersFormatsInternal(); + return codec; } - return FFMpegCache.ContainerFormats.Values.ToList().AsReadOnly(); - } + return null; + }); - public static bool TryGetContainerFormat(string name, out ContainerFormat fmt) - { - if (!GlobalFFOptions.Current.UseCache) - { - fmt = GetContainersFormatsInternal().FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim()); - return fmt != null; - } - else - { - return FFMpegCache.ContainerFormats.TryGetValue(name, out fmt); - } - } - - public static ContainerFormat GetContainerFormat(string name) - { - if (TryGetContainerFormat(name, out var fmt)) - { - return fmt; - } - - throw new FFMpegException(FFMpegExceptionType.Operation, $"Container format \"{name}\" not supported"); - } - #endregion - - private static void Cleanup(IEnumerable pathList) - { - foreach (var path in pathList) - { - if (File.Exists(path)) - { - File.Delete(path); - } - } - } + return res; } + + public static IReadOnlyList GetCodecs() + { + if (!GlobalFFOptions.Current.UseCache) + { + return GetCodecsInternal().Values.ToList().AsReadOnly(); + } + + return FFMpegCache.Codecs.Values.ToList().AsReadOnly(); + } + + public static IReadOnlyList GetCodecs(CodecType type) + { + if (!GlobalFFOptions.Current.UseCache) + { + return GetCodecsInternal().Values.Where(x => x.Type == type).ToList().AsReadOnly(); + } + + return FFMpegCache.Codecs.Values.Where(x => x.Type == type).ToList().AsReadOnly(); + } + + public static IReadOnlyList GetVideoCodecs() + { + return GetCodecs(CodecType.Video); + } + + public static IReadOnlyList GetAudioCodecs() + { + return GetCodecs(CodecType.Audio); + } + + public static IReadOnlyList GetSubtitleCodecs() + { + return GetCodecs(CodecType.Subtitle); + } + + public static IReadOnlyList GetDataCodecs() + { + return GetCodecs(CodecType.Data); + } + + public static bool TryGetCodec(string name, out Codec codec) + { + if (!GlobalFFOptions.Current.UseCache) + { + codec = GetCodecsInternal().Values.FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim()); + return codec != null; + } + + return FFMpegCache.Codecs.TryGetValue(name, out codec); + } + + public static Codec GetCodec(string name) + { + if (TryGetCodec(name, out var codec) && codec != null) + { + return codec; + } + + throw new FFMpegException(FFMpegExceptionType.Operation, $"Codec \"{name}\" not supported"); + } + + #endregion + + #region ContainerFormats + + internal static IReadOnlyList GetContainersFormatsInternal() + { + FFMpegHelper.RootExceptionCheck(); + + var list = new List(); + var instance = new ProcessArguments(GlobalFFOptions.GetFFMpegBinaryPath(), "-formats"); + instance.OutputDataReceived += (e, data) => + { + if (ContainerFormat.TryParse(data, out var fmt)) + { + list.Add(fmt); + } + }; + + var result = instance.StartAndWaitForExit(); + if (result.ExitCode != 0) + { + throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\r\n", result.OutputData)); + } + + return list.AsReadOnly(); + } + + public static IReadOnlyList GetContainerFormats() + { + if (!GlobalFFOptions.Current.UseCache) + { + return GetContainersFormatsInternal(); + } + + return FFMpegCache.ContainerFormats.Values.ToList().AsReadOnly(); + } + + public static bool TryGetContainerFormat(string name, out ContainerFormat fmt) + { + if (!GlobalFFOptions.Current.UseCache) + { + fmt = GetContainersFormatsInternal().FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim()); + return fmt != null; + } + + return FFMpegCache.ContainerFormats.TryGetValue(name, out fmt); + } + + public static ContainerFormat GetContainerFormat(string name) + { + if (TryGetContainerFormat(name, out var fmt)) + { + return fmt; + } + + throw new FFMpegException(FFMpegExceptionType.Operation, $"Container format \"{name}\" not supported"); + } + + #endregion } diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs index 941c1c2..7ee10e2 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs @@ -2,90 +2,275 @@ using FFMpegCore.Arguments; using FFMpegCore.Enums; -namespace FFMpegCore +namespace FFMpegCore; + +public class FFMpegArgumentOptions : FFMpegArgumentsBase { - public class FFMpegArgumentOptions : FFMpegArgumentsBase + internal FFMpegArgumentOptions() { } + + public FFMpegArgumentOptions WithAudioCodec(Codec audioCodec) { - internal FFMpegArgumentOptions() { } + return WithArgument(new AudioCodecArgument(audioCodec)); + } - public FFMpegArgumentOptions WithAudioCodec(Codec audioCodec) => WithArgument(new AudioCodecArgument(audioCodec)); - public FFMpegArgumentOptions WithAudioCodec(string audioCodec) => WithArgument(new AudioCodecArgument(audioCodec)); - public FFMpegArgumentOptions WithAudioBitrate(AudioQuality audioQuality) => WithArgument(new AudioBitrateArgument(audioQuality)); - public FFMpegArgumentOptions WithAudioBitrate(int bitrate) => WithArgument(new AudioBitrateArgument(bitrate)); - public FFMpegArgumentOptions WithAudioSamplingRate(int samplingRate = 48000) => WithArgument(new AudioSamplingRateArgument(samplingRate)); - public FFMpegArgumentOptions WithVariableBitrate(int vbr) => WithArgument(new VariableBitRateArgument(vbr)); - public FFMpegArgumentOptions Resize(int width, int height) => WithArgument(new SizeArgument(width, height)); - public FFMpegArgumentOptions Resize(Size? size) => WithArgument(new SizeArgument(size)); - public FFMpegArgumentOptions Crop(Size? size, int left, int top) => WithArgument(new CropArgument(size, top, left)); - public FFMpegArgumentOptions Crop(int width, int height, int left, int top) => WithArgument(new CropArgument(new Size(width, height), top, left)); - public FFMpegArgumentOptions Crop(Size? size) => WithArgument(new CropArgument(size, 0, 0)); - public FFMpegArgumentOptions Crop(int width, int height) => WithArgument(new CropArgument(new Size(width, height), 0, 0)); - public FFMpegArgumentOptions WithBitStreamFilter(Channel channel, Filter filter) => WithArgument(new BitStreamFilterArgument(channel, filter)); - public FFMpegArgumentOptions WithConstantRateFactor(int crf) => WithArgument(new ConstantRateFactorArgument(crf)); - public FFMpegArgumentOptions CopyChannel(Channel channel = Channel.Both) => WithArgument(new CopyArgument(channel)); - public FFMpegArgumentOptions DisableChannel(Channel channel) => WithArgument(new DisableChannelArgument(channel)); - public FFMpegArgumentOptions WithDuration(TimeSpan? duration) => WithArgument(new DurationArgument(duration)); - public FFMpegArgumentOptions WithFastStart() => WithArgument(new FaststartArgument()); - public FFMpegArgumentOptions WithFrameOutputCount(int frames) => WithArgument(new FrameOutputCountArgument(frames)); - public FFMpegArgumentOptions WithHardwareAcceleration(HardwareAccelerationDevice hardwareAccelerationDevice = HardwareAccelerationDevice.Auto) => WithArgument(new HardwareAccelerationArgument(hardwareAccelerationDevice)); + public FFMpegArgumentOptions WithAudioCodec(string audioCodec) + { + return WithArgument(new AudioCodecArgument(audioCodec)); + } - public FFMpegArgumentOptions UsingShortest(bool shortest = true) => WithArgument(new ShortestArgument(shortest)); - public FFMpegArgumentOptions UsingMultithreading(bool multithread) => WithArgument(new ThreadsArgument(multithread)); - public FFMpegArgumentOptions UsingThreads(int threads) => WithArgument(new ThreadsArgument(threads)); + public FFMpegArgumentOptions WithAudioBitrate(AudioQuality audioQuality) + { + return WithArgument(new AudioBitrateArgument(audioQuality)); + } - public FFMpegArgumentOptions WithVideoCodec(Codec videoCodec) => WithArgument(new VideoCodecArgument(videoCodec)); - public FFMpegArgumentOptions WithVideoCodec(string videoCodec) => WithArgument(new VideoCodecArgument(videoCodec)); - public FFMpegArgumentOptions WithVideoBitrate(int bitrate) => WithArgument(new VideoBitrateArgument(bitrate)); - public FFMpegArgumentOptions WithVideoFilters(Action videoFilterOptions) - { - var videoFilterOptionsObj = new VideoFilterOptions(); - videoFilterOptions(videoFilterOptionsObj); - return WithArgument(new VideoFiltersArgument(videoFilterOptionsObj)); - } + public FFMpegArgumentOptions WithAudioBitrate(int bitrate) + { + return WithArgument(new AudioBitrateArgument(bitrate)); + } - public FFMpegArgumentOptions WithAudioFilters(Action audioFilterOptions) - { - var audioFilterOptionsObj = new AudioFilterOptions(); - audioFilterOptions(audioFilterOptionsObj); - return WithArgument(new AudioFiltersArgument(audioFilterOptionsObj)); - } + public FFMpegArgumentOptions WithAudioSamplingRate(int samplingRate = 48000) + { + return WithArgument(new AudioSamplingRateArgument(samplingRate)); + } - public FFMpegArgumentOptions WithFramerate(double framerate) => WithArgument(new FrameRateArgument(framerate)); - public FFMpegArgumentOptions WithoutMetadata() => WithArgument(new RemoveMetadataArgument()); - public FFMpegArgumentOptions WithSpeedPreset(Speed speed) => WithArgument(new SpeedPresetArgument(speed)); - public FFMpegArgumentOptions WithStartNumber(int startNumber) => WithArgument(new StartNumberArgument(startNumber)); - public FFMpegArgumentOptions WithCustomArgument(string argument) => WithArgument(new CustomArgument(argument)); + public FFMpegArgumentOptions WithVariableBitrate(int vbr) + { + return WithArgument(new VariableBitRateArgument(vbr)); + } - public FFMpegArgumentOptions Seek(TimeSpan? seekTo) => WithArgument(new SeekArgument(seekTo)); - public FFMpegArgumentOptions EndSeek(TimeSpan? seekTo) => WithArgument(new EndSeekArgument(seekTo)); - public FFMpegArgumentOptions Loop(int times) => WithArgument(new LoopArgument(times)); - public FFMpegArgumentOptions OverwriteExisting() => WithArgument(new OverwriteArgument()); - public FFMpegArgumentOptions SelectStream(int streamIndex, int inputFileIndex = 0, - Channel channel = Channel.All) => WithArgument(new MapStreamArgument(streamIndex, inputFileIndex, channel)); - public FFMpegArgumentOptions SelectStreams(IEnumerable streamIndices, int inputFileIndex = 0, - Channel channel = Channel.All) => streamIndices.Aggregate(this, + public FFMpegArgumentOptions Resize(int width, int height) + { + return WithArgument(new SizeArgument(width, height)); + } + + public FFMpegArgumentOptions Resize(Size? size) + { + return WithArgument(new SizeArgument(size)); + } + + public FFMpegArgumentOptions Crop(Size? size, int left, int top) + { + return WithArgument(new CropArgument(size, top, left)); + } + + public FFMpegArgumentOptions Crop(int width, int height, int left, int top) + { + return WithArgument(new CropArgument(new Size(width, height), top, left)); + } + + public FFMpegArgumentOptions Crop(Size? size) + { + return WithArgument(new CropArgument(size, 0, 0)); + } + + public FFMpegArgumentOptions Crop(int width, int height) + { + return WithArgument(new CropArgument(new Size(width, height), 0, 0)); + } + + public FFMpegArgumentOptions WithBitStreamFilter(Channel channel, Filter filter) + { + return WithArgument(new BitStreamFilterArgument(channel, filter)); + } + + public FFMpegArgumentOptions WithConstantRateFactor(int crf) + { + return WithArgument(new ConstantRateFactorArgument(crf)); + } + + public FFMpegArgumentOptions CopyChannel(Channel channel = Channel.Both) + { + return WithArgument(new CopyArgument(channel)); + } + + public FFMpegArgumentOptions DisableChannel(Channel channel) + { + return WithArgument(new DisableChannelArgument(channel)); + } + + public FFMpegArgumentOptions WithDuration(TimeSpan? duration) + { + return WithArgument(new DurationArgument(duration)); + } + + public FFMpegArgumentOptions WithFastStart() + { + return WithArgument(new FaststartArgument()); + } + + public FFMpegArgumentOptions WithFrameOutputCount(int frames) + { + return WithArgument(new FrameOutputCountArgument(frames)); + } + + public FFMpegArgumentOptions WithHardwareAcceleration(HardwareAccelerationDevice hardwareAccelerationDevice = HardwareAccelerationDevice.Auto) + { + return WithArgument(new HardwareAccelerationArgument(hardwareAccelerationDevice)); + } + + public FFMpegArgumentOptions UsingShortest(bool shortest = true) + { + return WithArgument(new ShortestArgument(shortest)); + } + + public FFMpegArgumentOptions UsingMultithreading(bool multithread) + { + return WithArgument(new ThreadsArgument(multithread)); + } + + public FFMpegArgumentOptions UsingThreads(int threads) + { + return WithArgument(new ThreadsArgument(threads)); + } + + public FFMpegArgumentOptions WithVideoCodec(Codec videoCodec) + { + return WithArgument(new VideoCodecArgument(videoCodec)); + } + + public FFMpegArgumentOptions WithVideoCodec(string videoCodec) + { + return WithArgument(new VideoCodecArgument(videoCodec)); + } + + public FFMpegArgumentOptions WithVideoBitrate(int bitrate) + { + return WithArgument(new VideoBitrateArgument(bitrate)); + } + + public FFMpegArgumentOptions WithVideoFilters(Action videoFilterOptions) + { + var videoFilterOptionsObj = new VideoFilterOptions(); + videoFilterOptions(videoFilterOptionsObj); + return WithArgument(new VideoFiltersArgument(videoFilterOptionsObj)); + } + + public FFMpegArgumentOptions WithAudioFilters(Action audioFilterOptions) + { + var audioFilterOptionsObj = new AudioFilterOptions(); + audioFilterOptions(audioFilterOptionsObj); + return WithArgument(new AudioFiltersArgument(audioFilterOptionsObj)); + } + + public FFMpegArgumentOptions WithFramerate(double framerate) + { + return WithArgument(new FrameRateArgument(framerate)); + } + + public FFMpegArgumentOptions WithoutMetadata() + { + return WithArgument(new RemoveMetadataArgument()); + } + + public FFMpegArgumentOptions WithSpeedPreset(Speed speed) + { + return WithArgument(new SpeedPresetArgument(speed)); + } + + public FFMpegArgumentOptions WithStartNumber(int startNumber) + { + return WithArgument(new StartNumberArgument(startNumber)); + } + + public FFMpegArgumentOptions WithCustomArgument(string argument) + { + return WithArgument(new CustomArgument(argument)); + } + + public FFMpegArgumentOptions Seek(TimeSpan? seekTo) + { + return WithArgument(new SeekArgument(seekTo)); + } + + public FFMpegArgumentOptions EndSeek(TimeSpan? seekTo) + { + return WithArgument(new EndSeekArgument(seekTo)); + } + + public FFMpegArgumentOptions Loop(int times) + { + return WithArgument(new LoopArgument(times)); + } + + public FFMpegArgumentOptions OverwriteExisting() + { + return WithArgument(new OverwriteArgument()); + } + + public FFMpegArgumentOptions SelectStream(int streamIndex, int inputFileIndex = 0, + Channel channel = Channel.All) + { + return WithArgument(new MapStreamArgument(streamIndex, inputFileIndex, channel)); + } + + public FFMpegArgumentOptions SelectStreams(IEnumerable streamIndices, int inputFileIndex = 0, + Channel channel = Channel.All) + { + return streamIndices.Aggregate(this, (options, streamIndex) => options.SelectStream(streamIndex, inputFileIndex, channel)); - public FFMpegArgumentOptions DeselectStream(int streamIndex, int inputFileIndex = 0, - Channel channel = Channel.All) => WithArgument(new MapStreamArgument(streamIndex, inputFileIndex, channel, true)); - public FFMpegArgumentOptions DeselectStreams(IEnumerable streamIndices, int inputFileIndex = 0, - Channel channel = Channel.All) => streamIndices.Aggregate(this, + } + + public FFMpegArgumentOptions DeselectStream(int streamIndex, int inputFileIndex = 0, + Channel channel = Channel.All) + { + return WithArgument(new MapStreamArgument(streamIndex, inputFileIndex, channel, true)); + } + + public FFMpegArgumentOptions DeselectStreams(IEnumerable streamIndices, int inputFileIndex = 0, + Channel channel = Channel.All) + { + return streamIndices.Aggregate(this, (options, streamIndex) => options.DeselectStream(streamIndex, inputFileIndex, channel)); + } - public FFMpegArgumentOptions ForceFormat(ContainerFormat format) => WithArgument(new ForceFormatArgument(format)); - public FFMpegArgumentOptions ForceFormat(string format) => WithArgument(new ForceFormatArgument(format)); - public FFMpegArgumentOptions ForcePixelFormat(string pixelFormat) => WithArgument(new ForcePixelFormat(pixelFormat)); - public FFMpegArgumentOptions ForcePixelFormat(PixelFormat pixelFormat) => WithArgument(new ForcePixelFormat(pixelFormat)); + public FFMpegArgumentOptions ForceFormat(ContainerFormat format) + { + return WithArgument(new ForceFormatArgument(format)); + } - public FFMpegArgumentOptions WithAudibleEncryptionKeys(string key, string iv) => WithArgument(new AudibleEncryptionKeyArgument(key, iv)); - public FFMpegArgumentOptions WithAudibleActivationBytes(string activationBytes) => WithArgument(new AudibleEncryptionKeyArgument(activationBytes)); - public FFMpegArgumentOptions WithTagVersion(int id3v2Version = 3) => WithArgument(new ID3V2VersionArgument(id3v2Version)); - public FFMpegArgumentOptions WithGifPaletteArgument(int streamIndex, Size? size, int fps = 12) => WithArgument(new GifPaletteArgument(streamIndex, fps, size)); - public FFMpegArgumentOptions WithCopyCodec() => WithArgument(new CopyCodecArgument()); + public FFMpegArgumentOptions ForceFormat(string format) + { + return WithArgument(new ForceFormatArgument(format)); + } - public FFMpegArgumentOptions WithArgument(IArgument argument) - { - Arguments.Add(argument); - return this; - } + public FFMpegArgumentOptions ForcePixelFormat(string pixelFormat) + { + return WithArgument(new ForcePixelFormat(pixelFormat)); + } + + public FFMpegArgumentOptions ForcePixelFormat(PixelFormat pixelFormat) + { + return WithArgument(new ForcePixelFormat(pixelFormat)); + } + + public FFMpegArgumentOptions WithAudibleEncryptionKeys(string key, string iv) + { + return WithArgument(new AudibleEncryptionKeyArgument(key, iv)); + } + + public FFMpegArgumentOptions WithAudibleActivationBytes(string activationBytes) + { + return WithArgument(new AudibleEncryptionKeyArgument(activationBytes)); + } + + public FFMpegArgumentOptions WithTagVersion(int id3v2Version = 3) + { + return WithArgument(new ID3V2VersionArgument(id3v2Version)); + } + + public FFMpegArgumentOptions WithGifPaletteArgument(int streamIndex, Size? size, int fps = 12) + { + return WithArgument(new GifPaletteArgument(streamIndex, fps, size)); + } + + public FFMpegArgumentOptions WithCopyCodec() + { + return WithArgument(new CopyCodecArgument()); + } + + public FFMpegArgumentOptions WithArgument(IArgument argument) + { + Arguments.Add(argument); + return this; } } diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs index 3a58704..590c099 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs @@ -5,279 +5,286 @@ using FFMpegCore.Exceptions; using FFMpegCore.Helpers; using Instances; -namespace FFMpegCore +namespace FFMpegCore; + +public class FFMpegArgumentProcessor { - public class FFMpegArgumentProcessor + private static readonly Regex ProgressRegex = new(@"time=(\d\d:\d\d:\d\d.\d\d?)", RegexOptions.Compiled); + private readonly List> _configurations; + private readonly FFMpegArguments _ffMpegArguments; + private FFMpegLogLevel? _logLevel; + private Action? _onError; + private Action? _onOutput; + private Action? _onPercentageProgress; + private Action? _onTimeProgress; + private TimeSpan? _totalTimespan; + + internal FFMpegArgumentProcessor(FFMpegArguments ffMpegArguments) { - private static readonly Regex ProgressRegex = new(@"time=(\d\d:\d\d:\d\d.\d\d?)", RegexOptions.Compiled); - private readonly List> _configurations; - private readonly FFMpegArguments _ffMpegArguments; - private Action? _onPercentageProgress; - private Action? _onTimeProgress; - private Action? _onOutput; - private Action? _onError; - private TimeSpan? _totalTimespan; - private FFMpegLogLevel? _logLevel; + _configurations = new List>(); + _ffMpegArguments = ffMpegArguments; + } - internal FFMpegArgumentProcessor(FFMpegArguments ffMpegArguments) - { - _configurations = new List>(); - _ffMpegArguments = ffMpegArguments; - } + public string Arguments => _ffMpegArguments.Text; - public string Arguments => _ffMpegArguments.Text; + private event EventHandler CancelEvent = null!; - private event EventHandler CancelEvent = null!; + /// + /// Register action that will be invoked during the ffmpeg processing, when a progress time is output and parsed and progress percentage is + /// calculated. + /// Total time is needed to calculate the percentage that has been processed of the full file. + /// + /// Action to invoke when progress percentage is updated + /// The total timespan of the mediafile being processed + public FFMpegArgumentProcessor NotifyOnProgress(Action onPercentageProgress, TimeSpan totalTimeSpan) + { + _totalTimespan = totalTimeSpan; + _onPercentageProgress = onPercentageProgress; + return this; + } - /// - /// Register action that will be invoked during the ffmpeg processing, when a progress time is output and parsed and progress percentage is calculated. - /// Total time is needed to calculate the percentage that has been processed of the full file. - /// - /// Action to invoke when progress percentage is updated - /// The total timespan of the mediafile being processed - public FFMpegArgumentProcessor NotifyOnProgress(Action onPercentageProgress, TimeSpan totalTimeSpan) - { - _totalTimespan = totalTimeSpan; - _onPercentageProgress = onPercentageProgress; - return this; - } - /// - /// Register action that will be invoked during the ffmpeg processing, when a progress time is output and parsed - /// - /// Action that will be invoked with the parsed timestamp as argument - public FFMpegArgumentProcessor NotifyOnProgress(Action onTimeProgress) - { - _onTimeProgress = onTimeProgress; - return this; - } + /// + /// Register action that will be invoked during the ffmpeg processing, when a progress time is output and parsed + /// + /// Action that will be invoked with the parsed timestamp as argument + public FFMpegArgumentProcessor NotifyOnProgress(Action onTimeProgress) + { + _onTimeProgress = onTimeProgress; + return this; + } - /// - /// Register action that will be invoked during the ffmpeg processing, when a line is output - /// - /// - public FFMpegArgumentProcessor NotifyOnOutput(Action onOutput) - { - _onOutput = onOutput; - return this; - } - public FFMpegArgumentProcessor NotifyOnError(Action onError) - { - _onError = onError; - return this; - } - public FFMpegArgumentProcessor CancellableThrough(out Action cancel, int timeout = 0) - { - cancel = () => CancelEvent?.Invoke(this, timeout); - return this; - } - public FFMpegArgumentProcessor CancellableThrough(CancellationToken token, int timeout = 0) - { - token.Register(() => CancelEvent?.Invoke(this, timeout)); - return this; - } - public FFMpegArgumentProcessor Configure(Action configureOptions) - { - _configurations.Add(configureOptions); - return this; - } + /// + /// Register action that will be invoked during the ffmpeg processing, when a line is output + /// + /// + public FFMpegArgumentProcessor NotifyOnOutput(Action onOutput) + { + _onOutput = onOutput; + return this; + } - /// - /// Sets the log level of this process. Overides the - /// that is set in the for this specific process. - /// - /// The log level of the ffmpeg execution. - public FFMpegArgumentProcessor WithLogLevel(FFMpegLogLevel logLevel) + public FFMpegArgumentProcessor NotifyOnError(Action onError) + { + _onError = onError; + return this; + } + + public FFMpegArgumentProcessor CancellableThrough(out Action cancel, int timeout = 0) + { + cancel = () => CancelEvent?.Invoke(this, timeout); + return this; + } + + public FFMpegArgumentProcessor CancellableThrough(CancellationToken token, int timeout = 0) + { + token.Register(() => CancelEvent?.Invoke(this, timeout)); + return this; + } + + public FFMpegArgumentProcessor Configure(Action configureOptions) + { + _configurations.Add(configureOptions); + return this; + } + + /// + /// Sets the log level of this process. Overides the + /// that is set in the for this specific process. + /// + /// The log level of the ffmpeg execution. + public FFMpegArgumentProcessor WithLogLevel(FFMpegLogLevel logLevel) + { + _logLevel = logLevel; + return this; + } + + public bool ProcessSynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null) + { + var options = GetConfiguredOptions(ffMpegOptions); + var processArguments = PrepareProcessArguments(options, out var cancellationTokenSource); + + IProcessResult? processResult = null; + try { - _logLevel = logLevel; - return this; + processResult = Process(processArguments, cancellationTokenSource).ConfigureAwait(false).GetAwaiter().GetResult(); } - - public bool ProcessSynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null) + catch (OperationCanceledException) { - var options = GetConfiguredOptions(ffMpegOptions); - var processArguments = PrepareProcessArguments(options, out var cancellationTokenSource); - - IProcessResult? processResult = null; - try + if (throwOnError) { - processResult = Process(processArguments, cancellationTokenSource).ConfigureAwait(false).GetAwaiter().GetResult(); - } - catch (OperationCanceledException) - { - if (throwOnError) - { - throw; - } - } - - return HandleCompletion(throwOnError, processResult?.ExitCode ?? -1, processResult?.ErrorData ?? Array.Empty()); - } - - public async Task ProcessAsynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null) - { - var options = GetConfiguredOptions(ffMpegOptions); - var processArguments = PrepareProcessArguments(options, out var cancellationTokenSource); - - IProcessResult? processResult = null; - try - { - processResult = await Process(processArguments, cancellationTokenSource).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - if (throwOnError) - { - throw; - } - } - - return HandleCompletion(throwOnError, processResult?.ExitCode ?? -1, processResult?.ErrorData ?? Array.Empty()); - } - - private async Task Process(ProcessArguments processArguments, CancellationTokenSource cancellationTokenSource) - { - IProcessResult processResult = null!; - - _ffMpegArguments.Pre(); - - using var instance = processArguments.Start(); - var cancelled = false; - void OnCancelEvent(object sender, int timeout) - { - cancelled = true; - instance.SendInput("q"); - - if (!cancellationTokenSource.Token.WaitHandle.WaitOne(timeout, true)) - { - cancellationTokenSource.Cancel(); - instance.Kill(); - } - } - - CancelEvent += OnCancelEvent; - - try - { - await Task.WhenAll(instance.WaitForExitAsync().ContinueWith(t => - { - processResult = t.Result; - cancellationTokenSource.Cancel(); - _ffMpegArguments.Post(); - }), _ffMpegArguments.During(cancellationTokenSource.Token)).ConfigureAwait(false); - - if (cancelled) - { - throw new OperationCanceledException("ffmpeg processing was cancelled"); - } - - return processResult; - } - finally - { - CancelEvent -= OnCancelEvent; + throw; } } - private bool HandleCompletion(bool throwOnError, int exitCode, IReadOnlyList errorData) + return HandleCompletion(throwOnError, processResult?.ExitCode ?? -1, processResult?.ErrorData ?? Array.Empty()); + } + + public async Task ProcessAsynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null) + { + var options = GetConfiguredOptions(ffMpegOptions); + var processArguments = PrepareProcessArguments(options, out var cancellationTokenSource); + + IProcessResult? processResult = null; + try { - if (throwOnError && exitCode != 0) + processResult = await Process(processArguments, cancellationTokenSource).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + if (throwOnError) { - throw new FFMpegException(FFMpegExceptionType.Process, $"ffmpeg exited with non-zero exit-code ({exitCode} - {string.Join("\n", errorData)})", null, string.Join("\n", errorData)); + throw; } - - _onPercentageProgress?.Invoke(100.0); - if (_totalTimespan.HasValue) - { - _onTimeProgress?.Invoke(_totalTimespan.Value); - } - - return exitCode == 0; } - internal FFOptions GetConfiguredOptions(FFOptions? ffOptions) + return HandleCompletion(throwOnError, processResult?.ExitCode ?? -1, processResult?.ErrorData ?? Array.Empty()); + } + + private async Task Process(ProcessArguments processArguments, CancellationTokenSource cancellationTokenSource) + { + IProcessResult processResult = null!; + + _ffMpegArguments.Pre(); + + using var instance = processArguments.Start(); + var cancelled = false; + + void OnCancelEvent(object sender, int timeout) { - var options = ffOptions ?? GlobalFFOptions.Current.Clone(); + cancelled = true; + instance.SendInput("q"); - foreach (var configureOptions in _configurations) + if (!cancellationTokenSource.Token.WaitHandle.WaitOne(timeout, true)) { - configureOptions(options); + cancellationTokenSource.Cancel(); + instance.Kill(); } - - return options; } - private ProcessArguments PrepareProcessArguments(FFOptions ffOptions, - out CancellationTokenSource cancellationTokenSource) + CancelEvent += OnCancelEvent; + + try { - FFMpegHelper.RootExceptionCheck(); - FFMpegHelper.VerifyFFMpegExists(ffOptions); - - var arguments = _ffMpegArguments.Text; - - //If local loglevel is null, set the global. - if (_logLevel == null) + await Task.WhenAll(instance.WaitForExitAsync().ContinueWith(t => { - _logLevel = ffOptions.LogLevel; + processResult = t.Result; + cancellationTokenSource.Cancel(); + _ffMpegArguments.Post(); + }), _ffMpegArguments.During(cancellationTokenSource.Token)).ConfigureAwait(false); + + if (cancelled) + { + throw new OperationCanceledException("ffmpeg processing was cancelled"); } - //If neither local nor global loglevel is null, set the argument. - if (_logLevel != null) - { - var normalizedLogLevel = _logLevel.ToString() - .ToLower(); - arguments += $" -v {normalizedLogLevel}"; - } - - var startInfo = new ProcessStartInfo - { - FileName = GlobalFFOptions.GetFFMpegBinaryPath(ffOptions), - Arguments = arguments, - StandardOutputEncoding = ffOptions.Encoding, - StandardErrorEncoding = ffOptions.Encoding, - WorkingDirectory = ffOptions.WorkingDirectory - }; - var processArguments = new ProcessArguments(startInfo); - cancellationTokenSource = new CancellationTokenSource(); - - if (_onOutput != null) - { - processArguments.OutputDataReceived += OutputData; - } - - if (_onError != null || _onTimeProgress != null || (_onPercentageProgress != null && _totalTimespan != null)) - { - processArguments.ErrorDataReceived += ErrorData; - } - - return processArguments; + return processResult; } - - private void ErrorData(object sender, string msg) + finally { - _onError?.Invoke(msg); - - var match = ProgressRegex.Match(msg); - if (!match.Success) - { - return; - } - - var processed = MediaAnalysisUtils.ParseDuration(match.Groups[1].Value); - _onTimeProgress?.Invoke(processed); - - if (_onPercentageProgress == null || _totalTimespan == null) - { - return; - } - - var percentage = Math.Round(processed.TotalSeconds / _totalTimespan.Value.TotalSeconds * 100, 2); - _onPercentageProgress(percentage); - } - - private void OutputData(object sender, string msg) - { - Debug.WriteLine(msg); - _onOutput?.Invoke(msg); + CancelEvent -= OnCancelEvent; } } + + private bool HandleCompletion(bool throwOnError, int exitCode, IReadOnlyList errorData) + { + if (throwOnError && exitCode != 0) + { + throw new FFMpegException(FFMpegExceptionType.Process, $"ffmpeg exited with non-zero exit-code ({exitCode} - {string.Join("\n", errorData)})", null, + string.Join("\n", errorData)); + } + + _onPercentageProgress?.Invoke(100.0); + if (_totalTimespan.HasValue) + { + _onTimeProgress?.Invoke(_totalTimespan.Value); + } + + return exitCode == 0; + } + + internal FFOptions GetConfiguredOptions(FFOptions? ffOptions) + { + var options = ffOptions ?? GlobalFFOptions.Current.Clone(); + + foreach (var configureOptions in _configurations) + { + configureOptions(options); + } + + return options; + } + + private ProcessArguments PrepareProcessArguments(FFOptions ffOptions, + out CancellationTokenSource cancellationTokenSource) + { + FFMpegHelper.RootExceptionCheck(); + FFMpegHelper.VerifyFFMpegExists(ffOptions); + + var arguments = _ffMpegArguments.Text; + + //If local loglevel is null, set the global. + if (_logLevel == null) + { + _logLevel = ffOptions.LogLevel; + } + + //If neither local nor global loglevel is null, set the argument. + if (_logLevel != null) + { + var normalizedLogLevel = _logLevel.ToString() + .ToLower(); + arguments += $" -v {normalizedLogLevel}"; + } + + var startInfo = new ProcessStartInfo + { + FileName = GlobalFFOptions.GetFFMpegBinaryPath(ffOptions), + Arguments = arguments, + StandardOutputEncoding = ffOptions.Encoding, + StandardErrorEncoding = ffOptions.Encoding, + WorkingDirectory = ffOptions.WorkingDirectory + }; + var processArguments = new ProcessArguments(startInfo); + cancellationTokenSource = new CancellationTokenSource(); + + if (_onOutput != null) + { + processArguments.OutputDataReceived += OutputData; + } + + if (_onError != null || _onTimeProgress != null || (_onPercentageProgress != null && _totalTimespan != null)) + { + processArguments.ErrorDataReceived += ErrorData; + } + + return processArguments; + } + + private void ErrorData(object sender, string msg) + { + _onError?.Invoke(msg); + + var match = ProgressRegex.Match(msg); + if (!match.Success) + { + return; + } + + var processed = MediaAnalysisUtils.ParseDuration(match.Groups[1].Value); + _onTimeProgress?.Invoke(processed); + + if (_onPercentageProgress == null || _totalTimespan == null) + { + return; + } + + var percentage = Math.Round(processed.TotalSeconds / _totalTimespan.Value.TotalSeconds * 100, 2); + _onPercentageProgress(percentage); + } + + private void OutputData(object sender, string msg) + { + Debug.WriteLine(msg); + _onOutput?.Invoke(msg); + } } diff --git a/FFMpegCore/FFMpeg/FFMpegArguments.cs b/FFMpegCore/FFMpeg/FFMpegArguments.cs index ddb1f72..4c3b7bf 100644 --- a/FFMpegCore/FFMpeg/FFMpegArguments.cs +++ b/FFMpegCore/FFMpeg/FFMpegArguments.cs @@ -2,110 +2,199 @@ using FFMpegCore.Builders.MetaData; using FFMpegCore.Pipes; -namespace FFMpegCore +namespace FFMpegCore; + +public sealed class FFMpegArguments : FFMpegArgumentsBase { - public sealed class FFMpegArguments : FFMpegArgumentsBase + private readonly FFMpegGlobalArguments _globalArguments = new(); + + private FFMpegArguments() { } + + public string Text => GetText(); + + private string GetText() { - private readonly FFMpegGlobalArguments _globalArguments = new(); + var allArguments = _globalArguments.Arguments.Concat(Arguments).ToArray(); + return string.Join(" ", allArguments.Select(arg => arg is IDynamicArgument dynArg ? dynArg.GetText(allArguments) : arg.Text)); + } - private FFMpegArguments() { } + public static FFMpegArguments FromConcatInput(IEnumerable filePaths, Action? addArguments = null) + { + return new FFMpegArguments().WithInput(new ConcatArgument(filePaths), addArguments); + } - public string Text => GetText(); + public static FFMpegArguments FromDemuxConcatInput(IEnumerable filePaths, Action? addArguments = null) + { + return new FFMpegArguments().WithInput(new DemuxConcatArgument(filePaths), addArguments); + } - private string GetText() + public static FFMpegArguments FromFileInput(string filePath, bool verifyExists = true, Action? addArguments = null) + { + return new FFMpegArguments().WithInput(new InputArgument(verifyExists, filePath), addArguments); + } + + public static FFMpegArguments FromFileInput(IEnumerable filePath, bool verifyExists = true, Action? addArguments = null) + { + return new FFMpegArguments().WithInput(new MultiInputArgument(verifyExists, filePath), addArguments); + } + + public static FFMpegArguments FromFileInput(FileInfo fileInfo, Action? addArguments = null) + { + return new FFMpegArguments().WithInput(new InputArgument(fileInfo.FullName, false), addArguments); + } + + public static FFMpegArguments FromUrlInput(Uri uri, Action? addArguments = null) + { + return new FFMpegArguments().WithInput(new InputArgument(uri.AbsoluteUri, false), addArguments); + } + + public static FFMpegArguments FromDeviceInput(string device, Action? addArguments = null) + { + return new FFMpegArguments().WithInput(new InputDeviceArgument(device), addArguments); + } + + public static FFMpegArguments FromPipeInput(IPipeSource sourcePipe, Action? addArguments = null) + { + return new FFMpegArguments().WithInput(new InputPipeArgument(sourcePipe), addArguments); + } + + public FFMpegArguments WithGlobalOptions(Action configureOptions) + { + configureOptions(_globalArguments); + return this; + } + + public FFMpegArguments AddConcatInput(IEnumerable filePaths, Action? addArguments = null) + { + return WithInput(new ConcatArgument(filePaths), addArguments); + } + + public FFMpegArguments AddDemuxConcatInput(IEnumerable filePaths, Action? addArguments = null) + { + return WithInput(new DemuxConcatArgument(filePaths), addArguments); + } + + public FFMpegArguments AddFileInput(string filePath, bool verifyExists = true, Action? addArguments = null) + { + return WithInput(new InputArgument(verifyExists, filePath), addArguments); + } + + public FFMpegArguments AddFileInput(IEnumerable filePath, bool verifyExists = true, Action? addArguments = null) + { + return WithInput(new MultiInputArgument(verifyExists, filePath), addArguments); + } + + public FFMpegArguments AddFileInput(FileInfo fileInfo, Action? addArguments = null) + { + return WithInput(new InputArgument(fileInfo.FullName, false), addArguments); + } + + public FFMpegArguments AddUrlInput(Uri uri, Action? addArguments = null) + { + return WithInput(new InputArgument(uri.AbsoluteUri, false), addArguments); + } + + public FFMpegArguments AddDeviceInput(string device, Action? addArguments = null) + { + return WithInput(new InputDeviceArgument(device), addArguments); + } + + public FFMpegArguments AddPipeInput(IPipeSource sourcePipe, Action? addArguments = null) + { + return WithInput(new InputPipeArgument(sourcePipe), addArguments); + } + + public FFMpegArguments AddMetaData(string content, Action? addArguments = null) + { + return WithInput(new MetaDataArgument(content), addArguments); + } + + public FFMpegArguments AddMetaData(IReadOnlyMetaData metaData, Action? addArguments = null) + { + return WithInput(new MetaDataArgument(MetaDataSerializer.Instance.Serialize(metaData)), addArguments); + } + + /// + /// Maps the metadata of the given stream + /// + /// null means, the previous input will be used + public FFMpegArguments MapMetaData(int? inputIndex = null, Action? addArguments = null) + { + return WithInput(new MapMetadataArgument(inputIndex), addArguments); + } + + private FFMpegArguments WithInput(IInputArgument inputArgument, Action? addArguments) + { + var arguments = new FFMpegArgumentOptions(); + addArguments?.Invoke(arguments); + Arguments.AddRange(arguments.Arguments); + Arguments.Add(inputArgument); + return this; + } + + public FFMpegArgumentProcessor OutputToFile(string file, bool overwrite = true, Action? addArguments = null) + { + return ToProcessor(new OutputArgument(file, overwrite), addArguments); + } + + public FFMpegArgumentProcessor OutputToUrl(string uri, Action? addArguments = null) + { + return ToProcessor(new OutputUrlArgument(uri), addArguments); + } + + public FFMpegArgumentProcessor OutputToUrl(Uri uri, Action? addArguments = null) + { + return ToProcessor(new OutputUrlArgument(uri.ToString()), addArguments); + } + + public FFMpegArgumentProcessor OutputToPipe(IPipeSink reader, Action? addArguments = null) + { + return ToProcessor(new OutputPipeArgument(reader), addArguments); + } + + private FFMpegArgumentProcessor ToProcessor(IOutputArgument argument, Action? addArguments) + { + var args = new FFMpegArgumentOptions(); + addArguments?.Invoke(args); + Arguments.AddRange(args.Arguments); + Arguments.Add(argument); + return new FFMpegArgumentProcessor(this); + } + + public FFMpegArgumentProcessor OutputToTee(Action addOutputs, Action? addArguments = null) + { + var outputs = new FFMpegMultiOutputOptions(); + addOutputs(outputs); + return ToProcessor(new OutputTeeArgument(outputs), addArguments); + } + + public FFMpegArgumentProcessor MultiOutput(Action addOutputs) + { + var args = new FFMpegMultiOutputOptions(); + addOutputs(args); + Arguments.AddRange(args.Arguments); + return new FFMpegArgumentProcessor(this); + } + + internal void Pre() + { + foreach (var argument in Arguments.OfType()) { - var allArguments = _globalArguments.Arguments.Concat(Arguments).ToArray(); - return string.Join(" ", allArguments.Select(arg => arg is IDynamicArgument dynArg ? dynArg.GetText(allArguments) : arg.Text)); + argument.Pre(); } + } - public static FFMpegArguments FromConcatInput(IEnumerable filePaths, Action? addArguments = null) => new FFMpegArguments().WithInput(new ConcatArgument(filePaths), addArguments); - public static FFMpegArguments FromDemuxConcatInput(IEnumerable filePaths, Action? addArguments = null) => new FFMpegArguments().WithInput(new DemuxConcatArgument(filePaths), addArguments); - public static FFMpegArguments FromFileInput(string filePath, bool verifyExists = true, Action? addArguments = null) => new FFMpegArguments().WithInput(new InputArgument(verifyExists, filePath), addArguments); - public static FFMpegArguments FromFileInput(IEnumerable filePath, bool verifyExists = true, Action? addArguments = null) => new FFMpegArguments().WithInput(new MultiInputArgument(verifyExists, filePath), addArguments); - public static FFMpegArguments FromFileInput(FileInfo fileInfo, Action? addArguments = null) => new FFMpegArguments().WithInput(new InputArgument(fileInfo.FullName, false), addArguments); - public static FFMpegArguments FromUrlInput(Uri uri, Action? addArguments = null) => new FFMpegArguments().WithInput(new InputArgument(uri.AbsoluteUri, false), addArguments); - public static FFMpegArguments FromDeviceInput(string device, Action? addArguments = null) => new FFMpegArguments().WithInput(new InputDeviceArgument(device), addArguments); - public static FFMpegArguments FromPipeInput(IPipeSource sourcePipe, Action? addArguments = null) => new FFMpegArguments().WithInput(new InputPipeArgument(sourcePipe), addArguments); + internal async Task During(CancellationToken cancellationToken = default) + { + var inputOutputArguments = Arguments.OfType(); + await Task.WhenAll(inputOutputArguments.Select(io => io.During(cancellationToken))).ConfigureAwait(false); + } - public FFMpegArguments WithGlobalOptions(Action configureOptions) + internal void Post() + { + foreach (var argument in Arguments.OfType()) { - configureOptions(_globalArguments); - return this; - } - - public FFMpegArguments AddConcatInput(IEnumerable filePaths, Action? addArguments = null) => WithInput(new ConcatArgument(filePaths), addArguments); - public FFMpegArguments AddDemuxConcatInput(IEnumerable filePaths, Action? addArguments = null) => WithInput(new DemuxConcatArgument(filePaths), addArguments); - public FFMpegArguments AddFileInput(string filePath, bool verifyExists = true, Action? addArguments = null) => WithInput(new InputArgument(verifyExists, filePath), addArguments); - public FFMpegArguments AddFileInput(IEnumerable filePath, bool verifyExists = true, Action? addArguments = null) => WithInput(new MultiInputArgument(verifyExists, filePath), addArguments); - public FFMpegArguments AddFileInput(FileInfo fileInfo, Action? addArguments = null) => WithInput(new InputArgument(fileInfo.FullName, false), addArguments); - public FFMpegArguments AddUrlInput(Uri uri, Action? addArguments = null) => WithInput(new InputArgument(uri.AbsoluteUri, false), addArguments); - public FFMpegArguments AddDeviceInput(string device, Action? addArguments = null) => WithInput(new InputDeviceArgument(device), addArguments); - public FFMpegArguments AddPipeInput(IPipeSource sourcePipe, Action? addArguments = null) => WithInput(new InputPipeArgument(sourcePipe), addArguments); - public FFMpegArguments AddMetaData(string content, Action? addArguments = null) => WithInput(new MetaDataArgument(content), addArguments); - public FFMpegArguments AddMetaData(IReadOnlyMetaData metaData, Action? addArguments = null) => WithInput(new MetaDataArgument(MetaDataSerializer.Instance.Serialize(metaData)), addArguments); - - /// - /// Maps the metadata of the given stream - /// - /// null means, the previous input will be used - public FFMpegArguments MapMetaData(int? inputIndex = null, Action? addArguments = null) => WithInput(new MapMetadataArgument(inputIndex), addArguments); - - private FFMpegArguments WithInput(IInputArgument inputArgument, Action? addArguments) - { - var arguments = new FFMpegArgumentOptions(); - addArguments?.Invoke(arguments); - Arguments.AddRange(arguments.Arguments); - Arguments.Add(inputArgument); - return this; - } - - public FFMpegArgumentProcessor OutputToFile(string file, bool overwrite = true, Action? addArguments = null) => ToProcessor(new OutputArgument(file, overwrite), addArguments); - public FFMpegArgumentProcessor OutputToUrl(string uri, Action? addArguments = null) => ToProcessor(new OutputUrlArgument(uri), addArguments); - public FFMpegArgumentProcessor OutputToUrl(Uri uri, Action? addArguments = null) => ToProcessor(new OutputUrlArgument(uri.ToString()), addArguments); - public FFMpegArgumentProcessor OutputToPipe(IPipeSink reader, Action? addArguments = null) => ToProcessor(new OutputPipeArgument(reader), addArguments); - - private FFMpegArgumentProcessor ToProcessor(IOutputArgument argument, Action? addArguments) - { - var args = new FFMpegArgumentOptions(); - addArguments?.Invoke(args); - Arguments.AddRange(args.Arguments); - Arguments.Add(argument); - return new FFMpegArgumentProcessor(this); - } - - public FFMpegArgumentProcessor OutputToTee(Action addOutputs, Action? addArguments = null) - { - var outputs = new FFMpegMultiOutputOptions(); - addOutputs(outputs); - return ToProcessor(new OutputTeeArgument(outputs), addArguments); - } - - public FFMpegArgumentProcessor MultiOutput(Action addOutputs) - { - var args = new FFMpegMultiOutputOptions(); - addOutputs(args); - Arguments.AddRange(args.Arguments); - return new FFMpegArgumentProcessor(this); - } - - internal void Pre() - { - foreach (var argument in Arguments.OfType()) - { - argument.Pre(); - } - } - internal async Task During(CancellationToken cancellationToken = default) - { - var inputOutputArguments = Arguments.OfType(); - await Task.WhenAll(inputOutputArguments.Select(io => io.During(cancellationToken))).ConfigureAwait(false); - } - internal void Post() - { - foreach (var argument in Arguments.OfType()) - { - argument.Post(); - } + argument.Post(); } } } diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentsBase.cs b/FFMpegCore/FFMpeg/FFMpegArgumentsBase.cs index aae100a..3cd0224 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentsBase.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentsBase.cs @@ -1,9 +1,8 @@ using FFMpegCore.Arguments; -namespace FFMpegCore +namespace FFMpegCore; + +public abstract class FFMpegArgumentsBase { - public abstract class FFMpegArgumentsBase - { - internal readonly List Arguments = new(); - } + internal readonly List Arguments = new(); } diff --git a/FFMpegCore/FFMpeg/FFMpegCache.cs b/FFMpegCore/FFMpeg/FFMpegCache.cs index a9a6c23..1fe1153 100644 --- a/FFMpegCore/FFMpeg/FFMpegCache.cs +++ b/FFMpegCore/FFMpeg/FFMpegCache.cs @@ -1,67 +1,68 @@ using FFMpegCore.Enums; -namespace FFMpegCore +namespace FFMpegCore; + +internal static class FFMpegCache { - internal static class FFMpegCache + private static readonly object _syncObject = new(); + private static Dictionary? _pixelFormats; + private static Dictionary? _codecs; + private static Dictionary? _containers; + + public static IReadOnlyDictionary PixelFormats { - private static readonly object _syncObject = new(); - private static Dictionary? _pixelFormats; - private static Dictionary? _codecs; - private static Dictionary? _containers; - - public static IReadOnlyDictionary PixelFormats + get { - get + if (_pixelFormats == null) //First check not thread safe { - if (_pixelFormats == null) //First check not thread safe + lock (_syncObject) { - lock (_syncObject) + if (_pixelFormats == null) //Second check thread safe { - if (_pixelFormats == null)//Second check thread safe - { - _pixelFormats = FFMpeg.GetPixelFormatsInternal().ToDictionary(x => x.Name); - } + _pixelFormats = FFMpeg.GetPixelFormatsInternal().ToDictionary(x => x.Name); } } - - return _pixelFormats; } + + return _pixelFormats; } - public static IReadOnlyDictionary Codecs + } + + public static IReadOnlyDictionary Codecs + { + get { - get + if (_codecs == null) //First check not thread safe { - if (_codecs == null) //First check not thread safe + lock (_syncObject) { - lock (_syncObject) + if (_codecs == null) //Second check thread safe { - if (_codecs == null)//Second check thread safe - { - _codecs = FFMpeg.GetCodecsInternal(); - } + _codecs = FFMpeg.GetCodecsInternal(); } } - - return _codecs; } + + return _codecs; } - public static IReadOnlyDictionary ContainerFormats + } + + public static IReadOnlyDictionary ContainerFormats + { + get { - get + if (_containers == null) //First check not thread safe { - if (_containers == null) //First check not thread safe + lock (_syncObject) { - lock (_syncObject) + if (_containers == null) //Second check thread safe { - if (_containers == null)//Second check thread safe - { - _containers = FFMpeg.GetContainersFormatsInternal().ToDictionary(x => x.Name); - } + _containers = FFMpeg.GetContainersFormatsInternal().ToDictionary(x => x.Name); } } - - return _containers; } + + return _containers; } } } diff --git a/FFMpegCore/FFMpeg/FFMpegGlobalArguments.cs b/FFMpegCore/FFMpeg/FFMpegGlobalArguments.cs index ebbb658..069244a 100644 --- a/FFMpegCore/FFMpeg/FFMpegGlobalArguments.cs +++ b/FFMpegCore/FFMpeg/FFMpegGlobalArguments.cs @@ -1,17 +1,19 @@ using FFMpegCore.Arguments; -namespace FFMpegCore +namespace FFMpegCore; + +public sealed class FFMpegGlobalArguments : FFMpegArgumentsBase { - public sealed class FFMpegGlobalArguments : FFMpegArgumentsBase + internal FFMpegGlobalArguments() { } + + public FFMpegGlobalArguments WithVerbosityLevel(VerbosityLevel verbosityLevel = VerbosityLevel.Error) { - internal FFMpegGlobalArguments() { } + return WithOption(new VerbosityLevelArgument(verbosityLevel)); + } - public FFMpegGlobalArguments WithVerbosityLevel(VerbosityLevel verbosityLevel = VerbosityLevel.Error) => WithOption(new VerbosityLevelArgument(verbosityLevel)); - - private FFMpegGlobalArguments WithOption(IArgument argument) - { - Arguments.Add(argument); - return this; - } + private FFMpegGlobalArguments WithOption(IArgument argument) + { + Arguments.Add(argument); + return this; } } diff --git a/FFMpegCore/FFMpeg/FFMpegMultiOutputOptions.cs b/FFMpegCore/FFMpeg/FFMpegMultiOutputOptions.cs index 594413b..170c2e5 100644 --- a/FFMpegCore/FFMpeg/FFMpegMultiOutputOptions.cs +++ b/FFMpegCore/FFMpeg/FFMpegMultiOutputOptions.cs @@ -1,29 +1,40 @@ using FFMpegCore.Arguments; using FFMpegCore.Pipes; -namespace FFMpegCore +namespace FFMpegCore; + +public class FFMpegMultiOutputOptions { - public class FFMpegMultiOutputOptions + internal readonly List Outputs = new(); + + public IEnumerable Arguments => Outputs.SelectMany(o => o.Arguments); + + public FFMpegMultiOutputOptions OutputToFile(string file, bool overwrite = true, Action? addArguments = null) { - internal readonly List Outputs = new(); + return AddOutput(new OutputArgument(file, overwrite), addArguments); + } - public IEnumerable Arguments => Outputs.SelectMany(o => o.Arguments); + public FFMpegMultiOutputOptions OutputToUrl(string uri, Action? addArguments = null) + { + return AddOutput(new OutputUrlArgument(uri), addArguments); + } - public FFMpegMultiOutputOptions OutputToFile(string file, bool overwrite = true, Action? addArguments = null) => AddOutput(new OutputArgument(file, overwrite), addArguments); + public FFMpegMultiOutputOptions OutputToUrl(Uri uri, Action? addArguments = null) + { + return AddOutput(new OutputUrlArgument(uri.ToString()), addArguments); + } - public FFMpegMultiOutputOptions OutputToUrl(string uri, Action? addArguments = null) => AddOutput(new OutputUrlArgument(uri), addArguments); + public FFMpegMultiOutputOptions OutputToPipe(IPipeSink reader, Action? addArguments = null) + { + return AddOutput(new OutputPipeArgument(reader), addArguments); + } - public FFMpegMultiOutputOptions OutputToUrl(Uri uri, Action? addArguments = null) => AddOutput(new OutputUrlArgument(uri.ToString()), addArguments); - - public FFMpegMultiOutputOptions OutputToPipe(IPipeSink reader, Action? addArguments = null) => AddOutput(new OutputPipeArgument(reader), addArguments); - - public FFMpegMultiOutputOptions AddOutput(IOutputArgument argument, Action? addArguments) - { - var args = new FFMpegArgumentOptions(); - addArguments?.Invoke(args); - args.Arguments.Add(argument); - Outputs.Add(args); - return this; - } + public FFMpegMultiOutputOptions AddOutput(IOutputArgument argument, Action? addArguments) + { + var args = new FFMpegArgumentOptions(); + addArguments?.Invoke(args); + args.Arguments.Add(argument); + Outputs.Add(args); + return this; } } diff --git a/FFMpegCore/FFMpeg/Pipes/IAudioSample.cs b/FFMpegCore/FFMpeg/Pipes/IAudioSample.cs index 05f83ed..3681685 100644 --- a/FFMpegCore/FFMpeg/Pipes/IAudioSample.cs +++ b/FFMpegCore/FFMpeg/Pipes/IAudioSample.cs @@ -1,12 +1,11 @@ -namespace FFMpegCore.Pipes -{ - /// - /// Interface for Audio sample - /// - public interface IAudioSample - { - void Serialize(Stream stream); +namespace FFMpegCore.Pipes; - Task SerializeAsync(Stream stream, CancellationToken token); - } +/// +/// Interface for Audio sample +/// +public interface IAudioSample +{ + void Serialize(Stream stream); + + Task SerializeAsync(Stream stream, CancellationToken token); } diff --git a/FFMpegCore/FFMpeg/Pipes/IPipeSink.cs b/FFMpegCore/FFMpeg/Pipes/IPipeSink.cs index 1e1e6c3..c19bf00 100644 --- a/FFMpegCore/FFMpeg/Pipes/IPipeSink.cs +++ b/FFMpegCore/FFMpeg/Pipes/IPipeSink.cs @@ -1,8 +1,7 @@ -namespace FFMpegCore.Pipes +namespace FFMpegCore.Pipes; + +public interface IPipeSink { - public interface IPipeSink - { - Task ReadAsync(Stream inputStream, CancellationToken cancellationToken); - string GetFormat(); - } + Task ReadAsync(Stream inputStream, CancellationToken cancellationToken); + string GetFormat(); } diff --git a/FFMpegCore/FFMpeg/Pipes/IPipeSource.cs b/FFMpegCore/FFMpeg/Pipes/IPipeSource.cs index 33c0ab4..c9e55d3 100644 --- a/FFMpegCore/FFMpeg/Pipes/IPipeSource.cs +++ b/FFMpegCore/FFMpeg/Pipes/IPipeSource.cs @@ -1,11 +1,10 @@ -namespace FFMpegCore.Pipes +namespace FFMpegCore.Pipes; + +/// +/// Interface for ffmpeg pipe source data IO +/// +public interface IPipeSource { - /// - /// Interface for ffmpeg pipe source data IO - /// - public interface IPipeSource - { - string GetStreamArguments(); - Task WriteAsync(Stream outputStream, CancellationToken cancellationToken); - } + string GetStreamArguments(); + Task WriteAsync(Stream outputStream, CancellationToken cancellationToken); } diff --git a/FFMpegCore/FFMpeg/Pipes/IVideoFrame.cs b/FFMpegCore/FFMpeg/Pipes/IVideoFrame.cs index 9254dad..6cfc324 100644 --- a/FFMpegCore/FFMpeg/Pipes/IVideoFrame.cs +++ b/FFMpegCore/FFMpeg/Pipes/IVideoFrame.cs @@ -1,15 +1,14 @@ -namespace FFMpegCore.Pipes -{ - /// - /// Interface for Video frame - /// - public interface IVideoFrame - { - int Width { get; } - int Height { get; } - string Format { get; } +namespace FFMpegCore.Pipes; - void Serialize(Stream pipe); - Task SerializeAsync(Stream pipe, CancellationToken token); - } +/// +/// Interface for Video frame +/// +public interface IVideoFrame +{ + int Width { get; } + int Height { get; } + string Format { get; } + + void Serialize(Stream pipe); + Task SerializeAsync(Stream pipe, CancellationToken token); } diff --git a/FFMpegCore/FFMpeg/Pipes/PipeHelpers.cs b/FFMpegCore/FFMpeg/Pipes/PipeHelpers.cs index 108c146..01f3416 100644 --- a/FFMpegCore/FFMpeg/Pipes/PipeHelpers.cs +++ b/FFMpegCore/FFMpeg/Pipes/PipeHelpers.cs @@ -1,19 +1,21 @@ using System.Runtime.InteropServices; -namespace FFMpegCore.Pipes +namespace FFMpegCore.Pipes; + +internal static class PipeHelpers { - internal static class PipeHelpers + public static string GetUniquePipeName() { - public static string GetUnqiuePipeName() => $"FFMpegCore_{Guid.NewGuid().ToString("N").Substring(0, 5)}"; + return $"FFMpegCore_{Guid.NewGuid().ToString("N").Substring(0, 16)}"; + } - public static string GetPipePath(string pipeName) + public static string GetPipePath(string pipeName) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - return $@"\\.\pipe\{pipeName}"; - } - - return $"unix:{Path.Combine(Path.GetTempPath(), $"CoreFxPipe_{pipeName}")}"; + return $@"\\.\pipe\{pipeName}"; } + + return $"unix:{Path.Combine(Path.GetTempPath(), $"CoreFxPipe_{pipeName}")}"; } } diff --git a/FFMpegCore/FFMpeg/Pipes/RawAudioPipeSource.cs b/FFMpegCore/FFMpeg/Pipes/RawAudioPipeSource.cs index cfe31ef..a25241f 100644 --- a/FFMpegCore/FFMpeg/Pipes/RawAudioPipeSource.cs +++ b/FFMpegCore/FFMpeg/Pipes/RawAudioPipeSource.cs @@ -1,41 +1,42 @@ -namespace FFMpegCore.Pipes +namespace FFMpegCore.Pipes; + +/// +/// Implementation of for a raw audio stream that is gathered from . +/// It is the user's responbility to make sure the enumerated samples match the configuration provided to this pipe. +/// +public class RawAudioPipeSource : IPipeSource { - /// - /// Implementation of for a raw audio stream that is gathered from . - /// It is the user's responbility to make sure the enumerated samples match the configuration provided to this pipe. - /// - public class RawAudioPipeSource : IPipeSource + private readonly IEnumerator _sampleEnumerator; + + public RawAudioPipeSource(IEnumerator sampleEnumerator) { - private readonly IEnumerator _sampleEnumerator; + _sampleEnumerator = sampleEnumerator; + } - public string Format { get; set; } = "s16le"; - public uint SampleRate { get; set; } = 8000; - public uint Channels { get; set; } = 1; + public RawAudioPipeSource(IEnumerable sampleEnumerator) + : this(sampleEnumerator.GetEnumerator()) + { + } - public RawAudioPipeSource(IEnumerator sampleEnumerator) + public string Format { get; set; } = "s16le"; + public uint SampleRate { get; set; } = 8000; + public uint Channels { get; set; } = 1; + + public string GetStreamArguments() + { + return $"-f {Format} -ar {SampleRate} -ac {Channels}"; + } + + public async Task WriteAsync(Stream outputStream, CancellationToken cancellationToken) + { + if (_sampleEnumerator.MoveNext() && _sampleEnumerator.Current != null) { - _sampleEnumerator = sampleEnumerator; + await _sampleEnumerator.Current.SerializeAsync(outputStream, cancellationToken).ConfigureAwait(false); } - public RawAudioPipeSource(IEnumerable sampleEnumerator) - : this(sampleEnumerator.GetEnumerator()) { } - - public string GetStreamArguments() + while (_sampleEnumerator.MoveNext()) { - return $"-f {Format} -ar {SampleRate} -ac {Channels}"; - } - - public async Task WriteAsync(Stream outputStream, CancellationToken cancellationToken) - { - if (_sampleEnumerator.MoveNext() && _sampleEnumerator.Current != null) - { - await _sampleEnumerator.Current.SerializeAsync(outputStream, cancellationToken).ConfigureAwait(false); - } - - while (_sampleEnumerator.MoveNext()) - { - await _sampleEnumerator.Current!.SerializeAsync(outputStream, cancellationToken).ConfigureAwait(false); - } + await _sampleEnumerator.Current!.SerializeAsync(outputStream, cancellationToken).ConfigureAwait(false); } } } diff --git a/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs b/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs index 2f3028f..7b97064 100644 --- a/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs +++ b/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs @@ -1,71 +1,71 @@ using System.Globalization; using FFMpegCore.Exceptions; -namespace FFMpegCore.Pipes +namespace FFMpegCore.Pipes; + +/// +/// Implementation of for a raw video stream that is gathered from +/// +public class RawVideoPipeSource : IPipeSource { - /// - /// Implementation of for a raw video stream that is gathered from - /// - public class RawVideoPipeSource : IPipeSource + private readonly IEnumerator _framesEnumerator; + private bool _formatInitialized; + + public RawVideoPipeSource(IEnumerable framesEnumerator) { - public string StreamFormat { get; private set; } = null!; - public int Width { get; private set; } - public int Height { get; private set; } - public double FrameRate { get; set; } = 25; - private bool _formatInitialized; - private readonly IEnumerator _framesEnumerator; + _framesEnumerator = framesEnumerator.GetEnumerator(); + } - public RawVideoPipeSource(IEnumerable framesEnumerator) - { - _framesEnumerator = framesEnumerator.GetEnumerator(); - } + public string StreamFormat { get; private set; } = null!; + public int Width { get; private set; } + public int Height { get; private set; } + public double FrameRate { get; set; } = 25; - public string GetStreamArguments() + public string GetStreamArguments() + { + if (!_formatInitialized) { - if (!_formatInitialized) + //see input format references https://lists.ffmpeg.org/pipermail/ffmpeg-user/2012-July/007742.html + if (_framesEnumerator.Current == null) { - //see input format references https://lists.ffmpeg.org/pipermail/ffmpeg-user/2012-July/007742.html - if (_framesEnumerator.Current == null) + if (!_framesEnumerator.MoveNext()) { - if (!_framesEnumerator.MoveNext()) - { - throw new InvalidOperationException("Enumerator is empty, unable to get frame"); - } + throw new InvalidOperationException("Enumerator is empty, unable to get frame"); } - - StreamFormat = _framesEnumerator.Current!.Format; - Width = _framesEnumerator.Current!.Width; - Height = _framesEnumerator.Current!.Height; - - _formatInitialized = true; } - return $"-f rawvideo -r {FrameRate.ToString(CultureInfo.InvariantCulture)} -pix_fmt {StreamFormat} -s {Width}x{Height}"; + StreamFormat = _framesEnumerator.Current!.Format; + Width = _framesEnumerator.Current!.Width; + Height = _framesEnumerator.Current!.Height; + + _formatInitialized = true; } - public async Task WriteAsync(Stream outputStream, CancellationToken cancellationToken) - { - if (_framesEnumerator.Current != null) - { - CheckFrameAndThrow(_framesEnumerator.Current); - await _framesEnumerator.Current.SerializeAsync(outputStream, cancellationToken).ConfigureAwait(false); - } + return $"-f rawvideo -r {FrameRate.ToString(CultureInfo.InvariantCulture)} -pix_fmt {StreamFormat} -s {Width}x{Height}"; + } - while (_framesEnumerator.MoveNext()) - { - CheckFrameAndThrow(_framesEnumerator.Current!); - await _framesEnumerator.Current!.SerializeAsync(outputStream, cancellationToken).ConfigureAwait(false); - } + public async Task WriteAsync(Stream outputStream, CancellationToken cancellationToken) + { + if (_framesEnumerator.Current != null) + { + CheckFrameAndThrow(_framesEnumerator.Current); + await _framesEnumerator.Current.SerializeAsync(outputStream, cancellationToken).ConfigureAwait(false); } - private void CheckFrameAndThrow(IVideoFrame frame) + while (_framesEnumerator.MoveNext()) { - if (frame.Width != Width || frame.Height != Height || frame.Format != StreamFormat) - { - throw new FFMpegStreamFormatException(FFMpegExceptionType.Operation, "Video frame is not the same format as created raw video stream\r\n" + - $"Frame format: {frame.Width}x{frame.Height} pix_fmt: {frame.Format}\r\n" + - $"Stream format: {Width}x{Height} pix_fmt: {StreamFormat}"); - } + CheckFrameAndThrow(_framesEnumerator.Current!); + await _framesEnumerator.Current!.SerializeAsync(outputStream, cancellationToken).ConfigureAwait(false); + } + } + + private void CheckFrameAndThrow(IVideoFrame frame) + { + if (frame.Width != Width || frame.Height != Height || frame.Format != StreamFormat) + { + throw new FFMpegStreamFormatException(FFMpegExceptionType.Operation, "Video frame is not the same format as created raw video stream\r\n" + + $"Frame format: {frame.Width}x{frame.Height} pix_fmt: {frame.Format}\r\n" + + $"Stream format: {Width}x{Height} pix_fmt: {StreamFormat}"); } } } diff --git a/FFMpegCore/FFMpeg/Pipes/StreamPipeSink.cs b/FFMpegCore/FFMpeg/Pipes/StreamPipeSink.cs index 33b5747..102eb44 100644 --- a/FFMpegCore/FFMpeg/Pipes/StreamPipeSink.cs +++ b/FFMpegCore/FFMpeg/Pipes/StreamPipeSink.cs @@ -1,23 +1,28 @@ -namespace FFMpegCore.Pipes +namespace FFMpegCore.Pipes; + +public class StreamPipeSink : IPipeSink { - public class StreamPipeSink : IPipeSink + public StreamPipeSink(Func writer) { - public Func Writer { get; } - public int BlockSize { get; set; } = 4096; - public string Format { get; set; } = string.Empty; + Writer = writer; + } - public StreamPipeSink(Func writer) - { - Writer = writer; - } - public StreamPipeSink(Stream destination) - { - Writer = (inputStream, cancellationToken) => inputStream.CopyToAsync(destination, BlockSize, cancellationToken); - } + public StreamPipeSink(Stream destination) + { + Writer = (inputStream, cancellationToken) => inputStream.CopyToAsync(destination, BlockSize, cancellationToken); + } - public async Task ReadAsync(Stream inputStream, CancellationToken cancellationToken) - => await Writer(inputStream, cancellationToken).ConfigureAwait(false); + public Func Writer { get; } + public int BlockSize { get; set; } = 4096; + public string Format { get; set; } = string.Empty; - public string GetFormat() => Format; + public async Task ReadAsync(Stream inputStream, CancellationToken cancellationToken) + { + await Writer(inputStream, cancellationToken).ConfigureAwait(false); + } + + public string GetFormat() + { + return Format; } } diff --git a/FFMpegCore/FFMpeg/Pipes/StreamPipeSource.cs b/FFMpegCore/FFMpeg/Pipes/StreamPipeSource.cs index 87f621f..a3eb53f 100644 --- a/FFMpegCore/FFMpeg/Pipes/StreamPipeSource.cs +++ b/FFMpegCore/FFMpeg/Pipes/StreamPipeSource.cs @@ -1,21 +1,26 @@ -namespace FFMpegCore.Pipes +namespace FFMpegCore.Pipes; + +/// +/// Implementation of used for stream redirection +/// +public class StreamPipeSource : IPipeSource { - /// - /// Implementation of used for stream redirection - /// - public class StreamPipeSource : IPipeSource + public StreamPipeSource(Stream source) { - public Stream Source { get; } - public int BlockSize { get; } = 4096; - public string StreamFormat { get; } = string.Empty; + Source = source; + } - public StreamPipeSource(Stream source) - { - Source = source; - } + public Stream Source { get; } + public int BlockSize { get; } = 4096; + public string StreamFormat { get; } = string.Empty; - public string GetStreamArguments() => StreamFormat; + public string GetStreamArguments() + { + return StreamFormat; + } - public Task WriteAsync(Stream outputStream, CancellationToken cancellationToken) => Source.CopyToAsync(outputStream, BlockSize, cancellationToken); + public Task WriteAsync(Stream outputStream, CancellationToken cancellationToken) + { + return Source.CopyToAsync(outputStream, BlockSize, cancellationToken); } } diff --git a/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs b/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs index 7d83183..3204bcc 100644 --- a/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs +++ b/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs @@ -9,6 +9,30 @@ public static class SnapshotArgumentBuilder { public static (FFMpegArguments, Action outputOptions) BuildSnapshotArguments( string input, + string output, + IMediaAnalysis source, + Size? size = null, + TimeSpan? captureTime = null, + int? streamIndex = null, + int inputFileIndex = 0) + { + return BuildSnapshotArguments(input, VideoCodec.Image.GetByExtension(output), source, size, captureTime, streamIndex, inputFileIndex); + } + + public static (FFMpegArguments, Action outputOptions) BuildSnapshotArguments( + string input, + IMediaAnalysis source, + Size? size = null, + TimeSpan? captureTime = null, + int? streamIndex = null, + int inputFileIndex = 0) + { + return BuildSnapshotArguments(input, VideoCodec.Image.Png, source, size, captureTime, streamIndex, inputFileIndex); + } + + private static (FFMpegArguments, Action outputOptions) BuildSnapshotArguments( + string input, + Codec codec, IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null, @@ -26,7 +50,7 @@ public static class SnapshotArgumentBuilder .Seek(captureTime)), options => options .SelectStream((int)streamIndex, inputFileIndex) - .WithVideoCodec(VideoCodec.Png) + .WithVideoCodec(codec) .WithFrameOutputCount(1) .Resize(size)); } diff --git a/FFMpegCore/FFMpeg/bin/presets/ffprobe.xsd b/FFMpegCore/FFMpeg/bin/presets/ffprobe.xsd index 8a1e102..6ea703b 100644 --- a/FFMpegCore/FFMpeg/bin/presets/ffprobe.xsd +++ b/FFMpegCore/FFMpeg/bin/presets/ffprobe.xsd @@ -1,22 +1,22 @@ + xmlns:ffprobe="http://www.ffmpeg.org/schema/ffprobe" + targetNamespace="http://www.ffmpeg.org/schema/ffprobe"> - - - - - - - - - + + + + + + + + + @@ -33,53 +33,53 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - - + + + + + - - - - - - - - - - - + + + + + + + + + + + @@ -95,148 +95,148 @@ - - - - - - - - - - - + + + + + + + + + + + - - - - + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - + + + + - - - - - - - - - + + + + + + + + + - - - + + + - - - - - - - - + + + + + + + + - - + + - - + + - - - - - - - + + + + + + + - - - + + + - - - + + + - - - - - - + + + + + + - - - - - - + + + + + + - + diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index c324d62..671239d 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -3,27 +3,26 @@ true A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications - 5.2.0 + 5.3.0 ../nupkg - - **Instances and Packages Updates**: Updates to various instances and packages by rosenbjerg. -- **Audio and Video Enhancements**: Additions include a Copy option to Audio Codec and a Crop option to Arguments by brett-baker; video-stream level added to FFProbe analysis by Kaaybi; AV1 support for smaller snapshots and videos by BenediktBertsch; multiple input files support by AddyMills; HDR color properties support added to FFProbe analysis by Tomiscout. -- **System.Text.Json Bump**: Update by Kaaybi. -- **FFMpeg Processors and Utilities**: Modification for handling durations over 24 hours in `FFMpegArgumentProcessor` by alahane-techtel; fix for snapshots with correct width/height from rotated videos by Hagfjall. -- **Feature Additions and Fixes**: Support for multiple outputs and tee muxer by duggaraju; custom ffprob arguments by vfrz; fix for null reference exception with tags container by rosenbjerg; Chapter Modell change by vortex852456; codec copy added to the SaveM3U8Stream method by rpaschoal. -- **Closed and Non-merged Contributions**: Notable closed contributions include JSON source generators usage by onionware-github; Snapshot overload by 3UR; FromRawInput method by pedoc; runtime ffmpeg suite installation by yuqian5; and support for scale_npp by vicwilliam. -- **Miscellaneous Fixes**: Minor readme corrections by NaBian; fix for ffmpeg path issue by devedse. + + - **Fixed race condition on Named pipe dispose/disconnect** by techtel-pstevens + - **More extensions for snapshot function(jpg, bmp, webp)** by GorobVictor + - **Include more GUID characters in pipe path** by reima, rosenbjerg + - **Updated dependencies and minor cleanup**: by rosenbjerg + ffmpeg ffprobe convert video audio mediafile resize analyze muxing Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev README.md - + - - + + diff --git a/FFMpegCore/FFMpegCore.csproj.DotSettings b/FFMpegCore/FFMpegCore.csproj.DotSettings index 7a8d17a..2a3f49d 100644 --- a/FFMpegCore/FFMpegCore.csproj.DotSettings +++ b/FFMpegCore/FFMpegCore.csproj.DotSettings @@ -1,3 +1,5 @@ - + True - True \ No newline at end of file + True \ No newline at end of file diff --git a/FFMpegCore/FFOptions.cs b/FFMpegCore/FFOptions.cs index 2d4e4c9..ea784e5 100644 --- a/FFMpegCore/FFOptions.cs +++ b/FFMpegCore/FFOptions.cs @@ -2,68 +2,69 @@ using System.Text.Json.Serialization; using FFMpegCore.Enums; -namespace FFMpegCore +namespace FFMpegCore; + +public class FFOptions : ICloneable { - public class FFOptions : ICloneable + /// + /// Working directory for the ffmpeg/ffprobe instance + /// + public string WorkingDirectory { get; set; } = string.Empty; + + /// + /// Folder container ffmpeg and ffprobe binaries. Leave empty if ffmpeg and ffprobe are present in PATH + /// + public string BinaryFolder { get; set; } = string.Empty; + + /// + /// Folder used for temporary files necessary for static methods on FFMpeg class + /// + public string TemporaryFilesFolder { get; set; } = Path.GetTempPath(); + + /// + /// Encoding web name used to persist encoding + /// + public string EncodingWebName { get; set; } = Encoding.Default.WebName; + + /// + /// Encoding used for parsing stdout/stderr on ffmpeg and ffprobe processes + /// + [JsonIgnore] + public Encoding Encoding { - /// - /// Working directory for the ffmpeg/ffprobe instance - /// - public string WorkingDirectory { get; set; } = string.Empty; + get => Encoding.GetEncoding(EncodingWebName); + set => EncodingWebName = value?.WebName ?? Encoding.Default.WebName; + } - /// - /// Folder container ffmpeg and ffprobe binaries. Leave empty if ffmpeg and ffprobe are present in PATH - /// - public string BinaryFolder { get; set; } = string.Empty; + /// + /// The log level to use when calling of the ffmpeg executable. + /// + /// This option can be overridden before an execution of a Process command + /// to set the log level for that command. + /// + /// + public FFMpegLogLevel? LogLevel { get; set; } - /// - /// Folder used for temporary files necessary for static methods on FFMpeg class - /// - public string TemporaryFilesFolder { get; set; } = Path.GetTempPath(); + /// + /// + public Dictionary ExtensionOverrides { get; set; } = new() { { "mpegts", ".ts" } }; - /// - /// Encoding web name used to persist encoding - /// - public string EncodingWebName { get; set; } = Encoding.Default.WebName; + /// + /// Whether to cache calls to get ffmpeg codec, pixel- and container-formats + /// + public bool UseCache { get; set; } = true; - /// - /// Encoding used for parsing stdout/stderr on ffmpeg and ffprobe processes - /// - [JsonIgnore] - public Encoding Encoding - { - get => Encoding.GetEncoding(EncodingWebName); - set => EncodingWebName = value?.WebName ?? Encoding.Default.WebName; - } + /// + object ICloneable.Clone() + { + return Clone(); + } - /// - /// The log level to use when calling of the ffmpeg executable. - /// - /// This option can be overridden before an execution of a Process command - /// to set the log level for that command. - /// - /// - public FFMpegLogLevel? LogLevel { get; set; } - - /// - /// - /// - public Dictionary ExtensionOverrides { get; set; } = new() - { - { "mpegts", ".ts" }, - }; - - /// - /// Whether to cache calls to get ffmpeg codec, pixel- and container-formats - /// - public bool UseCache { get; set; } = true; - - /// - object ICloneable.Clone() => Clone(); - - /// - /// Creates a new object that is a copy of the current instance. - /// - public FFOptions Clone() => (FFOptions)MemberwiseClone(); + /// + /// Creates a new object that is a copy of the current instance. + /// + public FFOptions Clone() + { + return (FFOptions)MemberwiseClone(); } } diff --git a/FFMpegCore/FFProbe/AudioStream.cs b/FFMpegCore/FFProbe/AudioStream.cs index 871a78a..916afbd 100644 --- a/FFMpegCore/FFProbe/AudioStream.cs +++ b/FFMpegCore/FFProbe/AudioStream.cs @@ -1,10 +1,9 @@ -namespace FFMpegCore +namespace FFMpegCore; + +public class AudioStream : MediaStream { - public class AudioStream : MediaStream - { - public int Channels { get; set; } - public string ChannelLayout { get; set; } = null!; - public int SampleRateHz { get; set; } - public string Profile { get; set; } = null!; - } + public int Channels { get; set; } + public string ChannelLayout { get; set; } = null!; + public int SampleRateHz { get; set; } + public string Profile { get; set; } = null!; } diff --git a/FFMpegCore/FFProbe/Exceptions/FFProbeException.cs b/FFMpegCore/FFProbe/Exceptions/FFProbeException.cs index a17aec0..6f44caa 100644 --- a/FFMpegCore/FFProbe/Exceptions/FFProbeException.cs +++ b/FFMpegCore/FFProbe/Exceptions/FFProbeException.cs @@ -1,9 +1,8 @@ -namespace FFMpegCore.Exceptions +namespace FFMpegCore.Exceptions; + +public class FFProbeException : Exception { - public class FFProbeException : Exception + public FFProbeException(string message, Exception? inner = null) : base(message, inner) { - public FFProbeException(string message, Exception? inner = null) : base(message, inner) - { - } } } diff --git a/FFMpegCore/FFProbe/Exceptions/FFProbeProcessException.cs b/FFMpegCore/FFProbe/Exceptions/FFProbeProcessException.cs index cdbeb55..650b360 100644 --- a/FFMpegCore/FFProbe/Exceptions/FFProbeProcessException.cs +++ b/FFMpegCore/FFProbe/Exceptions/FFProbeProcessException.cs @@ -1,12 +1,11 @@ -namespace FFMpegCore.Exceptions -{ - public class FFProbeProcessException : FFProbeException - { - public IReadOnlyCollection ProcessErrors { get; } +namespace FFMpegCore.Exceptions; - public FFProbeProcessException(string message, IReadOnlyCollection processErrors, Exception? inner = null) : base(message, inner) - { - ProcessErrors = processErrors; - } +public class FFProbeProcessException : FFProbeException +{ + public FFProbeProcessException(string message, IReadOnlyCollection processErrors, Exception? inner = null) : base(message, inner) + { + ProcessErrors = processErrors; } + + public IReadOnlyCollection ProcessErrors { get; } } diff --git a/FFMpegCore/FFProbe/Exceptions/FormatNullException.cs b/FFMpegCore/FFProbe/Exceptions/FormatNullException.cs index 04ae0a0..7617d73 100644 --- a/FFMpegCore/FFProbe/Exceptions/FormatNullException.cs +++ b/FFMpegCore/FFProbe/Exceptions/FormatNullException.cs @@ -1,9 +1,8 @@ -namespace FFMpegCore.Exceptions +namespace FFMpegCore.Exceptions; + +public class FormatNullException : FFProbeException { - public class FormatNullException : FFProbeException + public FormatNullException() : base("Format not specified") { - public FormatNullException() : base("Format not specified") - { - } } } diff --git a/FFMpegCore/FFProbe/FFProbe.cs b/FFMpegCore/FFProbe/FFProbe.cs index ee5f5f5..e199376 100644 --- a/FFMpegCore/FFProbe/FFProbe.cs +++ b/FFMpegCore/FFProbe/FFProbe.cs @@ -1,235 +1,252 @@ using System.Diagnostics; using System.Text.Json; +using System.Text.Json.Serialization; using FFMpegCore.Arguments; using FFMpegCore.Exceptions; using FFMpegCore.Helpers; using FFMpegCore.Pipes; using Instances; -namespace FFMpegCore +namespace FFMpegCore; + +public static class FFProbe { - public static class FFProbe + public static IMediaAnalysis Analyse(string filePath, FFOptions? ffOptions = null, string? customArguments = null) { - public static IMediaAnalysis Analyse(string filePath, FFOptions? ffOptions = null, string? customArguments = null) + ThrowIfInputFileDoesNotExist(filePath); + + var processArguments = PrepareStreamAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments); + var result = processArguments.StartAndWaitForExit(); + ThrowIfExitCodeNotZero(result); + + return ParseOutput(result); + } + + public static FFProbeFrames GetFrames(string filePath, FFOptions? ffOptions = null, string? customArguments = null) + { + ThrowIfInputFileDoesNotExist(filePath); + + var instance = PrepareFrameAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments); + var result = instance.StartAndWaitForExit(); + ThrowIfExitCodeNotZero(result); + + return ParseFramesOutput(result); + } + + public static FFProbePackets GetPackets(string filePath, FFOptions? ffOptions = null, string? customArguments = null) + { + ThrowIfInputFileDoesNotExist(filePath); + + var instance = PreparePacketAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments); + var result = instance.StartAndWaitForExit(); + ThrowIfExitCodeNotZero(result); + + return ParsePacketsOutput(result); + } + + public static IMediaAnalysis Analyse(Uri uri, FFOptions? ffOptions = null, string? customArguments = null) + { + var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current, customArguments); + var result = instance.StartAndWaitForExit(); + ThrowIfExitCodeNotZero(result); + + return ParseOutput(result); + } + + public static IMediaAnalysis Analyse(Stream stream, FFOptions? ffOptions = null, string? customArguments = null) + { + var streamPipeSource = new StreamPipeSource(stream); + var pipeArgument = new InputPipeArgument(streamPipeSource); + var instance = PrepareStreamAnalysisInstance(pipeArgument.PipePath, ffOptions ?? GlobalFFOptions.Current, customArguments); + pipeArgument.Pre(); + + var task = instance.StartAndWaitForExitAsync(); + try { - ThrowIfInputFileDoesNotExist(filePath); - - var processArguments = PrepareStreamAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments); - var result = processArguments.StartAndWaitForExit(); - ThrowIfExitCodeNotZero(result); - - return ParseOutput(result); + pipeArgument.During().ConfigureAwait(false).GetAwaiter().GetResult(); } - - public static FFProbeFrames GetFrames(string filePath, FFOptions? ffOptions = null, string? customArguments = null) + catch (IOException) { } + finally { - ThrowIfInputFileDoesNotExist(filePath); - - var instance = PrepareFrameAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments); - var result = instance.StartAndWaitForExit(); - ThrowIfExitCodeNotZero(result); - - return ParseFramesOutput(result); - } - - public static FFProbePackets GetPackets(string filePath, FFOptions? ffOptions = null, string? customArguments = null) - { - ThrowIfInputFileDoesNotExist(filePath); - - var instance = PreparePacketAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments); - var result = instance.StartAndWaitForExit(); - ThrowIfExitCodeNotZero(result); - - return ParsePacketsOutput(result); - } - - public static IMediaAnalysis Analyse(Uri uri, FFOptions? ffOptions = null, string? customArguments = null) - { - var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current, customArguments); - var result = instance.StartAndWaitForExit(); - ThrowIfExitCodeNotZero(result); - - return ParseOutput(result); - } - public static IMediaAnalysis Analyse(Stream stream, FFOptions? ffOptions = null, string? customArguments = null) - { - var streamPipeSource = new StreamPipeSource(stream); - var pipeArgument = new InputPipeArgument(streamPipeSource); - var instance = PrepareStreamAnalysisInstance(pipeArgument.PipePath, ffOptions ?? GlobalFFOptions.Current, customArguments); - pipeArgument.Pre(); - - var task = instance.StartAndWaitForExitAsync(); - try - { - pipeArgument.During().ConfigureAwait(false).GetAwaiter().GetResult(); - } - catch (IOException) { } - finally - { - pipeArgument.Post(); - } - - var result = task.ConfigureAwait(false).GetAwaiter().GetResult(); - ThrowIfExitCodeNotZero(result); - - return ParseOutput(result); - } - - public static async Task AnalyseAsync(string filePath, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, string? customArguments = null) - { - ThrowIfInputFileDoesNotExist(filePath); - - var instance = PrepareStreamAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments); - var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); - ThrowIfExitCodeNotZero(result); - - return ParseOutput(result); - } - - public static FFProbeFrames GetFrames(Uri uri, FFOptions? ffOptions = null, string? customArguments = null) - { - var instance = PrepareFrameAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current, customArguments); - var result = instance.StartAndWaitForExit(); - ThrowIfExitCodeNotZero(result); - - return ParseFramesOutput(result); - } - - public static async Task GetFramesAsync(string filePath, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, string? customArguments = null) - { - ThrowIfInputFileDoesNotExist(filePath); - - var instance = PrepareFrameAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments); - var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); - return ParseFramesOutput(result); - } - - public static async Task GetPacketsAsync(string filePath, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, string? customArguments = null) - { - ThrowIfInputFileDoesNotExist(filePath); - - var instance = PreparePacketAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments); - var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); - return ParsePacketsOutput(result); - } - - public static async Task AnalyseAsync(Uri uri, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, string? customArguments = null) - { - var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current, customArguments); - var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); - ThrowIfExitCodeNotZero(result); - - return ParseOutput(result); - } - public static async Task AnalyseAsync(Stream stream, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, string? customArguments = null) - { - var streamPipeSource = new StreamPipeSource(stream); - var pipeArgument = new InputPipeArgument(streamPipeSource); - var instance = PrepareStreamAnalysisInstance(pipeArgument.PipePath, ffOptions ?? GlobalFFOptions.Current, customArguments); - pipeArgument.Pre(); - - var task = instance.StartAndWaitForExitAsync(cancellationToken); - try - { - await pipeArgument.During(cancellationToken).ConfigureAwait(false); - } - catch (IOException) - { - } - finally - { - pipeArgument.Post(); - } - - var result = await task.ConfigureAwait(false); - ThrowIfExitCodeNotZero(result); - pipeArgument.Post(); - return ParseOutput(result); } - public static async Task GetFramesAsync(Uri uri, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, string? customArguments = null) + var result = task.ConfigureAwait(false).GetAwaiter().GetResult(); + ThrowIfExitCodeNotZero(result); + + return ParseOutput(result); + } + + public static async Task AnalyseAsync(string filePath, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, + string? customArguments = null) + { + ThrowIfInputFileDoesNotExist(filePath); + + var instance = PrepareStreamAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments); + var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); + ThrowIfExitCodeNotZero(result); + + return ParseOutput(result); + } + + public static FFProbeFrames GetFrames(Uri uri, FFOptions? ffOptions = null, string? customArguments = null) + { + var instance = PrepareFrameAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current, customArguments); + var result = instance.StartAndWaitForExit(); + ThrowIfExitCodeNotZero(result); + + return ParseFramesOutput(result); + } + + public static async Task GetFramesAsync(string filePath, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, + string? customArguments = null) + { + ThrowIfInputFileDoesNotExist(filePath); + + var instance = PrepareFrameAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments); + var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); + return ParseFramesOutput(result); + } + + public static async Task GetPacketsAsync(string filePath, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, + string? customArguments = null) + { + ThrowIfInputFileDoesNotExist(filePath); + + var instance = PreparePacketAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current, customArguments); + var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); + return ParsePacketsOutput(result); + } + + public static async Task AnalyseAsync(Uri uri, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, + string? customArguments = null) + { + var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current, customArguments); + var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); + ThrowIfExitCodeNotZero(result); + + return ParseOutput(result); + } + + public static async Task AnalyseAsync(Stream stream, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, + string? customArguments = null) + { + var streamPipeSource = new StreamPipeSource(stream); + var pipeArgument = new InputPipeArgument(streamPipeSource); + var instance = PrepareStreamAnalysisInstance(pipeArgument.PipePath, ffOptions ?? GlobalFFOptions.Current, customArguments); + pipeArgument.Pre(); + + var task = instance.StartAndWaitForExitAsync(cancellationToken); + try { - var instance = PrepareFrameAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current, customArguments); - var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); - return ParseFramesOutput(result); + await pipeArgument.During(cancellationToken).ConfigureAwait(false); + } + catch (IOException) + { + } + finally + { + pipeArgument.Post(); } - private static IMediaAnalysis ParseOutput(IProcessResult instance) + var result = await task.ConfigureAwait(false); + ThrowIfExitCodeNotZero(result); + + pipeArgument.Post(); + return ParseOutput(result); + } + + public static async Task GetFramesAsync(Uri uri, FFOptions? ffOptions = null, CancellationToken cancellationToken = default, + string? customArguments = null) + { + var instance = PrepareFrameAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current, customArguments); + var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); + return ParseFramesOutput(result); + } + + private static IMediaAnalysis ParseOutput(IProcessResult instance) + { + var json = string.Join(string.Empty, instance.OutputData); + var ffprobeAnalysis = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + + if (ffprobeAnalysis?.Format == null) { - var json = string.Join(string.Empty, instance.OutputData); - var ffprobeAnalysis = JsonSerializer.Deserialize(json, new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true - }); - - if (ffprobeAnalysis?.Format == null) - { - throw new FormatNullException(); - } - - ffprobeAnalysis.ErrorData = instance.ErrorData; - return new MediaAnalysis(ffprobeAnalysis); + throw new FormatNullException(); } - private static FFProbeFrames ParseFramesOutput(IProcessResult instance) - { - var json = string.Join(string.Empty, instance.OutputData); - var ffprobeAnalysis = JsonSerializer.Deserialize(json, new JsonSerializerOptions + + ffprobeAnalysis.ErrorData = instance.ErrorData; + return new MediaAnalysis(ffprobeAnalysis); + } + + private static FFProbeFrames ParseFramesOutput(IProcessResult instance) + { + var json = string.Join(string.Empty, instance.OutputData); + var ffprobeAnalysis = JsonSerializer.Deserialize(json, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true, - NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString | System.Text.Json.Serialization.JsonNumberHandling.WriteAsString + NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString }); - return ffprobeAnalysis!; - } + return ffprobeAnalysis!; + } - private static FFProbePackets ParsePacketsOutput(IProcessResult instance) - { - var json = string.Join(string.Empty, instance.OutputData); - var ffprobeAnalysis = JsonSerializer.Deserialize(json, new JsonSerializerOptions + private static FFProbePackets ParsePacketsOutput(IProcessResult instance) + { + var json = string.Join(string.Empty, instance.OutputData); + var ffprobeAnalysis = JsonSerializer.Deserialize(json, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true, - NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString | System.Text.Json.Serialization.JsonNumberHandling.WriteAsString + NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString }); - return ffprobeAnalysis!; - } + return ffprobeAnalysis!; + } - private static void ThrowIfInputFileDoesNotExist(string filePath) + private static void ThrowIfInputFileDoesNotExist(string filePath) + { + if (!File.Exists(filePath)) { - if (!File.Exists(filePath)) - { - throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'"); - } - } - - private static void ThrowIfExitCodeNotZero(IProcessResult result) - { - if (result.ExitCode != 0) - { - var message = $"ffprobe exited with non-zero exit-code ({result.ExitCode} - {string.Join("\n", result.ErrorData)})"; - throw new FFMpegException(FFMpegExceptionType.Process, message, null, string.Join("\n", result.ErrorData)); - } - } - - private static ProcessArguments PrepareStreamAnalysisInstance(string filePath, FFOptions ffOptions, string? customArguments) - => PrepareInstance($"-loglevel error -print_format json -show_format -sexagesimal -show_streams -show_chapters \"{filePath}\"", ffOptions, customArguments); - private static ProcessArguments PrepareFrameAnalysisInstance(string filePath, FFOptions ffOptions, string? customArguments) - => PrepareInstance($"-loglevel error -print_format json -show_frames -v quiet -sexagesimal \"{filePath}\"", ffOptions, customArguments); - private static ProcessArguments PreparePacketAnalysisInstance(string filePath, FFOptions ffOptions, string? customArguments) - => PrepareInstance($"-loglevel error -print_format json -show_packets -v quiet -sexagesimal \"{filePath}\"", ffOptions, customArguments); - - private static ProcessArguments PrepareInstance(string arguments, FFOptions ffOptions, string? customArguments) - { - FFProbeHelper.RootExceptionCheck(); - FFProbeHelper.VerifyFFProbeExists(ffOptions); - var startInfo = new ProcessStartInfo(GlobalFFOptions.GetFFProbeBinaryPath(ffOptions), $"{arguments} {customArguments}") - { - StandardOutputEncoding = ffOptions.Encoding, - StandardErrorEncoding = ffOptions.Encoding, - WorkingDirectory = ffOptions.WorkingDirectory - }; - return new ProcessArguments(startInfo); + throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'"); } } + + private static void ThrowIfExitCodeNotZero(IProcessResult result) + { + if (result.ExitCode != 0) + { + var message = $"ffprobe exited with non-zero exit-code ({result.ExitCode} - {string.Join("\n", result.ErrorData)})"; + throw new FFMpegException(FFMpegExceptionType.Process, message, null, string.Join("\n", result.ErrorData)); + } + } + + private static ProcessArguments PrepareStreamAnalysisInstance(string filePath, FFOptions ffOptions, string? customArguments) + { + return PrepareInstance($"-loglevel error -print_format json -show_format -sexagesimal -show_streams -show_chapters \"{filePath}\"", ffOptions, + customArguments); + } + + private static ProcessArguments PrepareFrameAnalysisInstance(string filePath, FFOptions ffOptions, string? customArguments) + { + return PrepareInstance($"-loglevel error -print_format json -show_frames -v quiet -sexagesimal \"{filePath}\"", ffOptions, customArguments); + } + + private static ProcessArguments PreparePacketAnalysisInstance(string filePath, FFOptions ffOptions, string? customArguments) + { + return PrepareInstance($"-loglevel error -print_format json -show_packets -v quiet -sexagesimal \"{filePath}\"", ffOptions, customArguments); + } + + private static ProcessArguments PrepareInstance(string arguments, FFOptions ffOptions, string? customArguments) + { + FFProbeHelper.RootExceptionCheck(); + FFProbeHelper.VerifyFFProbeExists(ffOptions); + var startInfo = new ProcessStartInfo(GlobalFFOptions.GetFFProbeBinaryPath(ffOptions), $"{arguments} {customArguments}") + { + StandardOutputEncoding = ffOptions.Encoding, + StandardErrorEncoding = ffOptions.Encoding, + WorkingDirectory = ffOptions.WorkingDirectory + }; + return new ProcessArguments(startInfo); + } } diff --git a/FFMpegCore/FFProbe/FFProbeAnalysis.cs b/FFMpegCore/FFProbe/FFProbeAnalysis.cs index 9f8e880..3812ecf 100644 --- a/FFMpegCore/FFProbe/FFProbeAnalysis.cs +++ b/FFMpegCore/FFProbe/FFProbeAnalysis.cs @@ -1,217 +1,189 @@ using System.Text.Json.Nodes; using System.Text.Json.Serialization; -namespace FFMpegCore +namespace FFMpegCore; + +public class FFProbeAnalysis { - public class FFProbeAnalysis + [JsonPropertyName("streams")] public List Streams { get; set; } = null!; + + [JsonPropertyName("format")] public Format Format { get; set; } = null!; + + [JsonPropertyName("chapters")] public List Chapters { get; set; } = null!; + + [JsonIgnore] public IReadOnlyList ErrorData { get; set; } = new List(); +} + +public class FFProbeStream : ITagsContainer, IDispositionContainer +{ + [JsonPropertyName("index")] public int Index { get; set; } + + [JsonPropertyName("avg_frame_rate")] public string AvgFrameRate { get; set; } = null!; + + [JsonPropertyName("bits_per_raw_sample")] + public string BitsPerRawSample { get; set; } = null!; + + [JsonPropertyName("bits_per_sample")] public int BitsPerSample { get; set; } = 0; + + [JsonPropertyName("bit_rate")] public string BitRate { get; set; } = null!; + + [JsonPropertyName("channels")] public int? Channels { get; set; } + + [JsonPropertyName("channel_layout")] public string ChannelLayout { get; set; } = null!; + + [JsonPropertyName("codec_type")] public string CodecType { get; set; } = null!; + + [JsonPropertyName("codec_name")] public string CodecName { get; set; } = null!; + + [JsonPropertyName("codec_long_name")] public string CodecLongName { get; set; } = null!; + + [JsonPropertyName("codec_tag")] public string CodecTag { get; set; } = null!; + + [JsonPropertyName("codec_tag_string")] public string CodecTagString { get; set; } = null!; + + [JsonPropertyName("display_aspect_ratio")] + public string DisplayAspectRatio { get; set; } = null!; + + [JsonPropertyName("sample_aspect_ratio")] + public string SampleAspectRatio { get; set; } = null!; + + [JsonPropertyName("start_time")] public string StartTime { get; set; } = null!; + + [JsonPropertyName("duration")] public string Duration { get; set; } = null!; + + [JsonPropertyName("profile")] public string Profile { get; set; } = null!; + + [JsonPropertyName("width")] public int? Width { get; set; } + + [JsonPropertyName("height")] public int? Height { get; set; } + + [JsonPropertyName("r_frame_rate")] public string FrameRate { get; set; } = null!; + + [JsonPropertyName("pix_fmt")] public string PixelFormat { get; set; } = null!; + + [JsonPropertyName("level")] public int Level { get; set; } + + [JsonPropertyName("sample_rate")] public string SampleRate { get; set; } = null!; + + [JsonPropertyName("side_data_list")] public List> SideData { get; set; } = null!; + + [JsonPropertyName("color_range")] public string ColorRange { get; set; } = null!; + + [JsonPropertyName("color_space")] public string ColorSpace { get; set; } = null!; + + [JsonPropertyName("color_transfer")] public string ColorTransfer { get; set; } = null!; + + [JsonPropertyName("color_primaries")] public string ColorPrimaries { get; set; } = null!; + + [JsonPropertyName("disposition")] public Dictionary Disposition { get; set; } = null!; + + [JsonPropertyName("tags")] public Dictionary? Tags { get; set; } +} + +public class Format : ITagsContainer +{ + [JsonPropertyName("filename")] public string Filename { get; set; } = null!; + + [JsonPropertyName("nb_streams")] public int NbStreams { get; set; } + + [JsonPropertyName("nb_programs")] public int NbPrograms { get; set; } + + [JsonPropertyName("format_name")] public string FormatName { get; set; } = null!; + + [JsonPropertyName("format_long_name")] public string FormatLongName { get; set; } = null!; + + [JsonPropertyName("start_time")] public string StartTime { get; set; } = null!; + + [JsonPropertyName("duration")] public string Duration { get; set; } = null!; + + [JsonPropertyName("size")] public string Size { get; set; } = null!; + + [JsonPropertyName("bit_rate")] public string? BitRate { get; set; } = null!; + + [JsonPropertyName("probe_score")] public int ProbeScore { get; set; } + + [JsonPropertyName("tags")] public Dictionary? Tags { get; set; } +} + +public class Chapter : ITagsContainer +{ + [JsonPropertyName("id")] public long Id { get; set; } + + [JsonPropertyName("time_base")] public string TimeBase { get; set; } = null!; + + [JsonPropertyName("start")] public long Start { get; set; } + + [JsonPropertyName("start_time")] public string StartTime { get; set; } = null!; + + [JsonPropertyName("end")] public long End { get; set; } + + [JsonPropertyName("end_time")] public string EndTime { get; set; } = null!; + + [JsonPropertyName("tags")] public Dictionary? Tags { get; set; } +} + +public interface IDispositionContainer +{ + Dictionary Disposition { get; set; } +} + +public interface ITagsContainer +{ + Dictionary? Tags { get; set; } +} + +public static class TagExtensions +{ + private static string? TryGetTagValue(ITagsContainer tagsContainer, string key) { - [JsonPropertyName("streams")] - public List Streams { get; set; } = null!; - - [JsonPropertyName("format")] - public Format Format { get; set; } = null!; - - [JsonPropertyName("chapters")] - public List Chapters { get; set; } = null!; - - [JsonIgnore] - public IReadOnlyList ErrorData { get; set; } = new List(); - } - - public class FFProbeStream : ITagsContainer, IDispositionContainer - { - [JsonPropertyName("index")] - public int Index { get; set; } - - [JsonPropertyName("avg_frame_rate")] - public string AvgFrameRate { get; set; } = null!; - - [JsonPropertyName("bits_per_raw_sample")] - public string BitsPerRawSample { get; set; } = null!; - - [JsonPropertyName("bits_per_sample")] - public int BitsPerSample { get; set; } = 0; - - [JsonPropertyName("bit_rate")] - public string BitRate { get; set; } = null!; - - [JsonPropertyName("channels")] - public int? Channels { get; set; } - - [JsonPropertyName("channel_layout")] - public string ChannelLayout { get; set; } = null!; - - [JsonPropertyName("codec_type")] - public string CodecType { get; set; } = null!; - - [JsonPropertyName("codec_name")] - public string CodecName { get; set; } = null!; - - [JsonPropertyName("codec_long_name")] - public string CodecLongName { get; set; } = null!; - - [JsonPropertyName("codec_tag")] - public string CodecTag { get; set; } = null!; - - [JsonPropertyName("codec_tag_string")] - public string CodecTagString { get; set; } = null!; - - [JsonPropertyName("display_aspect_ratio")] - public string DisplayAspectRatio { get; set; } = null!; - - [JsonPropertyName("sample_aspect_ratio")] - public string SampleAspectRatio { get; set; } = null!; - - [JsonPropertyName("start_time")] - public string StartTime { get; set; } = null!; - - [JsonPropertyName("duration")] - public string Duration { get; set; } = null!; - - [JsonPropertyName("profile")] - public string Profile { get; set; } = null!; - - [JsonPropertyName("width")] - public int? Width { get; set; } - - [JsonPropertyName("height")] - public int? Height { get; set; } - - [JsonPropertyName("r_frame_rate")] - public string FrameRate { get; set; } = null!; - - [JsonPropertyName("pix_fmt")] - public string PixelFormat { get; set; } = null!; - - [JsonPropertyName("level")] - public int Level { get; set; } - - [JsonPropertyName("sample_rate")] - public string SampleRate { get; set; } = null!; - - [JsonPropertyName("disposition")] - public Dictionary Disposition { get; set; } = null!; - - [JsonPropertyName("tags")] - public Dictionary? Tags { get; set; } - - [JsonPropertyName("side_data_list")] - public List> SideData { get; set; } = null!; - - [JsonPropertyName("color_range")] - public string ColorRange { get; set; } = null!; - - [JsonPropertyName("color_space")] - public string ColorSpace { get; set; } = null!; - - [JsonPropertyName("color_transfer")] - public string ColorTransfer { get; set; } = null!; - - [JsonPropertyName("color_primaries")] - public string ColorPrimaries { get; set; } = null!; - } - - public class Format : ITagsContainer - { - [JsonPropertyName("filename")] - public string Filename { get; set; } = null!; - - [JsonPropertyName("nb_streams")] - public int NbStreams { get; set; } - - [JsonPropertyName("nb_programs")] - public int NbPrograms { get; set; } - - [JsonPropertyName("format_name")] - public string FormatName { get; set; } = null!; - - [JsonPropertyName("format_long_name")] - public string FormatLongName { get; set; } = null!; - - [JsonPropertyName("start_time")] - public string StartTime { get; set; } = null!; - - [JsonPropertyName("duration")] - public string Duration { get; set; } = null!; - - [JsonPropertyName("size")] - public string Size { get; set; } = null!; - - [JsonPropertyName("bit_rate")] - public string? BitRate { get; set; } = null!; - - [JsonPropertyName("probe_score")] - public int ProbeScore { get; set; } - - [JsonPropertyName("tags")] - public Dictionary? Tags { get; set; } - } - - public class Chapter : ITagsContainer - { - [JsonPropertyName("id")] - public long Id { get; set; } - - [JsonPropertyName("time_base")] - public string TimeBase { get; set; } = null!; - - [JsonPropertyName("start")] - public long Start { get; set; } - - [JsonPropertyName("start_time")] - public string StartTime { get; set; } = null!; - - [JsonPropertyName("end")] - public long End { get; set; } - - [JsonPropertyName("end_time")] - public string EndTime { get; set; } = null!; - - [JsonPropertyName("tags")] - public Dictionary? Tags { get; set; } - } - - public interface IDispositionContainer - { - Dictionary Disposition { get; set; } - } - - public interface ITagsContainer - { - Dictionary? Tags { get; set; } - } - - public static class TagExtensions - { - private static string? TryGetTagValue(ITagsContainer tagsContainer, string key) + if (tagsContainer.Tags != null && tagsContainer.Tags.TryGetValue(key, out var tagValue)) { - if (tagsContainer.Tags != null && tagsContainer.Tags.TryGetValue(key, out var tagValue)) - { - return tagValue; - } - - return null; + return tagValue; } - public static string? GetLanguage(this ITagsContainer tagsContainer) => TryGetTagValue(tagsContainer, "language"); - public static string? GetCreationTime(this ITagsContainer tagsContainer) => TryGetTagValue(tagsContainer, "creation_time "); - public static string? GetRotate(this ITagsContainer tagsContainer) => TryGetTagValue(tagsContainer, "rotate"); - public static string? GetDuration(this ITagsContainer tagsContainer) => TryGetTagValue(tagsContainer, "duration"); + return null; } - public static class DispositionExtensions + public static string? GetLanguage(this ITagsContainer tagsContainer) { - private static int? TryGetDispositionValue(IDispositionContainer dispositionContainer, string key) - { - if (dispositionContainer.Disposition != null && dispositionContainer.Disposition.TryGetValue(key, out var dispositionValue)) - { - return dispositionValue; - } + return TryGetTagValue(tagsContainer, "language"); + } - return null; - } + public static string? GetCreationTime(this ITagsContainer tagsContainer) + { + return TryGetTagValue(tagsContainer, "creation_time "); + } - public static int? GetDefault(this IDispositionContainer tagsContainer) => TryGetDispositionValue(tagsContainer, "default"); - public static int? GetForced(this IDispositionContainer tagsContainer) => TryGetDispositionValue(tagsContainer, "forced"); + public static string? GetRotate(this ITagsContainer tagsContainer) + { + return TryGetTagValue(tagsContainer, "rotate"); + } + + public static string? GetDuration(this ITagsContainer tagsContainer) + { + return TryGetTagValue(tagsContainer, "duration"); + } +} + +public static class DispositionExtensions +{ + private static int? TryGetDispositionValue(IDispositionContainer dispositionContainer, string key) + { + if (dispositionContainer.Disposition != null && dispositionContainer.Disposition.TryGetValue(key, out var dispositionValue)) + { + return dispositionValue; + } + + return null; + } + + public static int? GetDefault(this IDispositionContainer tagsContainer) + { + return TryGetDispositionValue(tagsContainer, "default"); + } + + public static int? GetForced(this IDispositionContainer tagsContainer) + { + return TryGetDispositionValue(tagsContainer, "forced"); } } diff --git a/FFMpegCore/FFProbe/FrameAnalysis.cs b/FFMpegCore/FFProbe/FrameAnalysis.cs index 68ef500..cdb91b4 100644 --- a/FFMpegCore/FFProbe/FrameAnalysis.cs +++ b/FFMpegCore/FFProbe/FrameAnalysis.cs @@ -1,82 +1,62 @@ using System.Text.Json.Serialization; -namespace FFMpegCore +namespace FFMpegCore; + +public class FFProbeFrameAnalysis { - public class FFProbeFrameAnalysis - { - [JsonPropertyName("media_type")] - public string MediaType { get; set; } = null!; + [JsonPropertyName("media_type")] public string MediaType { get; set; } = null!; - [JsonPropertyName("stream_index")] - public int StreamIndex { get; set; } + [JsonPropertyName("stream_index")] public int StreamIndex { get; set; } - [JsonPropertyName("key_frame")] - public int KeyFrame { get; set; } + [JsonPropertyName("key_frame")] public int KeyFrame { get; set; } - [JsonPropertyName("pkt_pts")] - public long PacketPts { get; set; } + [JsonPropertyName("pkt_pts")] public long PacketPts { get; set; } - [JsonPropertyName("pkt_pts_time")] - public string PacketPtsTime { get; set; } = null!; + [JsonPropertyName("pkt_pts_time")] public string PacketPtsTime { get; set; } = null!; - [JsonPropertyName("pkt_dts")] - public long PacketDts { get; set; } + [JsonPropertyName("pkt_dts")] public long PacketDts { get; set; } - [JsonPropertyName("pkt_dts_time")] - public string PacketDtsTime { get; set; } = null!; + [JsonPropertyName("pkt_dts_time")] public string PacketDtsTime { get; set; } = null!; - [JsonPropertyName("best_effort_timestamp")] - public long BestEffortTimestamp { get; set; } + [JsonPropertyName("best_effort_timestamp")] + public long BestEffortTimestamp { get; set; } - [JsonPropertyName("best_effort_timestamp_time")] - public string BestEffortTimestampTime { get; set; } = null!; + [JsonPropertyName("best_effort_timestamp_time")] + public string BestEffortTimestampTime { get; set; } = null!; - [JsonPropertyName("pkt_duration")] - public int PacketDuration { get; set; } + [JsonPropertyName("pkt_duration")] public int PacketDuration { get; set; } - [JsonPropertyName("pkt_duration_time")] - public string PacketDurationTime { get; set; } = null!; + [JsonPropertyName("pkt_duration_time")] + public string PacketDurationTime { get; set; } = null!; - [JsonPropertyName("pkt_pos")] - public long PacketPos { get; set; } + [JsonPropertyName("pkt_pos")] public long PacketPos { get; set; } - [JsonPropertyName("pkt_size")] - public int PacketSize { get; set; } + [JsonPropertyName("pkt_size")] public int PacketSize { get; set; } - [JsonPropertyName("width")] - public long Width { get; set; } + [JsonPropertyName("width")] public long Width { get; set; } - [JsonPropertyName("height")] - public long Height { get; set; } + [JsonPropertyName("height")] public long Height { get; set; } - [JsonPropertyName("pix_fmt")] - public string PixelFormat { get; set; } = null!; + [JsonPropertyName("pix_fmt")] public string PixelFormat { get; set; } = null!; - [JsonPropertyName("pict_type")] - public string PictureType { get; set; } = null!; + [JsonPropertyName("pict_type")] public string PictureType { get; set; } = null!; - [JsonPropertyName("coded_picture_number")] - public long CodedPictureNumber { get; set; } + [JsonPropertyName("coded_picture_number")] + public long CodedPictureNumber { get; set; } - [JsonPropertyName("display_picture_number")] - public long DisplayPictureNumber { get; set; } + [JsonPropertyName("display_picture_number")] + public long DisplayPictureNumber { get; set; } - [JsonPropertyName("interlaced_frame")] - public int InterlacedFrame { get; set; } + [JsonPropertyName("interlaced_frame")] public int InterlacedFrame { get; set; } - [JsonPropertyName("top_field_first")] - public int TopFieldFirst { get; set; } + [JsonPropertyName("top_field_first")] public int TopFieldFirst { get; set; } - [JsonPropertyName("repeat_pict")] - public int RepeatPicture { get; set; } + [JsonPropertyName("repeat_pict")] public int RepeatPicture { get; set; } - [JsonPropertyName("chroma_location")] - public string ChromaLocation { get; set; } = null!; - } - - public class FFProbeFrames - { - [JsonPropertyName("frames")] - public List Frames { get; set; } = null!; - } + [JsonPropertyName("chroma_location")] public string ChromaLocation { get; set; } = null!; +} + +public class FFProbeFrames +{ + [JsonPropertyName("frames")] public List Frames { get; set; } = null!; } diff --git a/FFMpegCore/FFProbe/IMediaAnalysis.cs b/FFMpegCore/FFProbe/IMediaAnalysis.cs index d99992a..72d1670 100644 --- a/FFMpegCore/FFProbe/IMediaAnalysis.cs +++ b/FFMpegCore/FFProbe/IMediaAnalysis.cs @@ -1,18 +1,17 @@ using FFMpegCore.Builders.MetaData; -namespace FFMpegCore +namespace FFMpegCore; + +public interface IMediaAnalysis { - public interface IMediaAnalysis - { - TimeSpan Duration { get; } - MediaFormat Format { get; } - List Chapters { get; } - AudioStream? PrimaryAudioStream { get; } - VideoStream? PrimaryVideoStream { get; } - SubtitleStream? PrimarySubtitleStream { get; } - List VideoStreams { get; } - List AudioStreams { get; } - List SubtitleStreams { get; } - IReadOnlyList ErrorData { get; } - } + TimeSpan Duration { get; } + MediaFormat Format { get; } + List Chapters { get; } + AudioStream? PrimaryAudioStream { get; } + VideoStream? PrimaryVideoStream { get; } + SubtitleStream? PrimarySubtitleStream { get; } + List VideoStreams { get; } + List AudioStreams { get; } + List SubtitleStreams { get; } + IReadOnlyList ErrorData { get; } } diff --git a/FFMpegCore/FFProbe/MediaAnalysis.cs b/FFMpegCore/FFProbe/MediaAnalysis.cs index bf16af6..4129e63 100644 --- a/FFMpegCore/FFProbe/MediaAnalysis.cs +++ b/FFMpegCore/FFProbe/MediaAnalysis.cs @@ -1,258 +1,262 @@ -using System.Text.RegularExpressions; +using System.Globalization; +using System.Text.RegularExpressions; using FFMpegCore.Builders.MetaData; -namespace FFMpegCore +namespace FFMpegCore; + +internal class MediaAnalysis : IMediaAnalysis { - internal class MediaAnalysis : IMediaAnalysis + internal MediaAnalysis(FFProbeAnalysis analysis) { - internal MediaAnalysis(FFProbeAnalysis analysis) - { - Format = ParseFormat(analysis.Format); - Chapters = analysis.Chapters.Select(c => ParseChapter(c)).ToList(); - VideoStreams = analysis.Streams.Where(stream => stream.CodecType == "video").Select(ParseVideoStream).ToList(); - AudioStreams = analysis.Streams.Where(stream => stream.CodecType == "audio").Select(ParseAudioStream).ToList(); - SubtitleStreams = analysis.Streams.Where(stream => stream.CodecType == "subtitle").Select(ParseSubtitleStream).ToList(); - ErrorData = analysis.ErrorData; - } - - private MediaFormat ParseFormat(Format analysisFormat) - { - return new MediaFormat - { - Duration = MediaAnalysisUtils.ParseDuration(analysisFormat.Duration), - StartTime = MediaAnalysisUtils.ParseDuration(analysisFormat.StartTime), - FormatName = analysisFormat.FormatName, - FormatLongName = analysisFormat.FormatLongName, - StreamCount = analysisFormat.NbStreams, - ProbeScore = analysisFormat.ProbeScore, - BitRate = long.Parse(analysisFormat.BitRate ?? "0"), - Tags = analysisFormat.Tags.ToCaseInsensitive(), - }; - } - - private string GetValue(string tagName, Dictionary? tags, string defaultValue) => - tags == null ? defaultValue : tags.TryGetValue(tagName, out var value) ? value : defaultValue; - - private ChapterData ParseChapter(Chapter analysisChapter) - { - var title = GetValue("title", analysisChapter.Tags, "TitleValueNotSet"); - var start = MediaAnalysisUtils.ParseDuration(analysisChapter.StartTime); - var end = MediaAnalysisUtils.ParseDuration(analysisChapter.EndTime); - - return new ChapterData(title, start, end); - } - - public TimeSpan Duration => new[] - { - Format.Duration, - PrimaryVideoStream?.Duration ?? TimeSpan.Zero, - PrimaryAudioStream?.Duration ?? TimeSpan.Zero - }.Max(); - - public MediaFormat Format { get; } - - public List Chapters { get; } - - public AudioStream? PrimaryAudioStream => AudioStreams.OrderBy(stream => stream.Index).FirstOrDefault(); - public VideoStream? PrimaryVideoStream => VideoStreams.OrderBy(stream => stream.Index).FirstOrDefault(); - public SubtitleStream? PrimarySubtitleStream => SubtitleStreams.OrderBy(stream => stream.Index).FirstOrDefault(); - - public List VideoStreams { get; } - public List AudioStreams { get; } - public List SubtitleStreams { get; } - public IReadOnlyList ErrorData { get; } - - private int? GetBitDepth(FFProbeStream stream) - { - var bitDepth = int.TryParse(stream.BitsPerRawSample, out var bprs) ? bprs : - stream.BitsPerSample; - return bitDepth == 0 ? null : bitDepth; - } - - private VideoStream ParseVideoStream(FFProbeStream stream) - { - return new VideoStream - { - Index = stream.Index, - AvgFrameRate = MediaAnalysisUtils.DivideRatio(MediaAnalysisUtils.ParseRatioDouble(stream.AvgFrameRate, '/')), - BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseLongInvariant(stream.BitRate) : default, - BitsPerRawSample = !string.IsNullOrEmpty(stream.BitsPerRawSample) ? MediaAnalysisUtils.ParseIntInvariant(stream.BitsPerRawSample) : default, - CodecName = stream.CodecName, - CodecLongName = stream.CodecLongName, - CodecTag = stream.CodecTag, - CodecTagString = stream.CodecTagString, - DisplayAspectRatio = MediaAnalysisUtils.ParseRatioInt(stream.DisplayAspectRatio, ':'), - SampleAspectRatio = MediaAnalysisUtils.ParseRatioInt(stream.SampleAspectRatio, ':'), - Duration = MediaAnalysisUtils.ParseDuration(stream.Duration), - StartTime = MediaAnalysisUtils.ParseDuration(stream.StartTime), - FrameRate = MediaAnalysisUtils.DivideRatio(MediaAnalysisUtils.ParseRatioDouble(stream.FrameRate, '/')), - Height = stream.Height ?? 0, - Width = stream.Width ?? 0, - Profile = stream.Profile, - PixelFormat = stream.PixelFormat, - Level = stream.Level, - ColorRange = stream.ColorRange, - ColorSpace = stream.ColorSpace, - ColorTransfer = stream.ColorTransfer, - ColorPrimaries = stream.ColorPrimaries, - Rotation = MediaAnalysisUtils.ParseRotation(stream), - Language = stream.GetLanguage(), - Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition), - Tags = stream.Tags.ToCaseInsensitive(), - BitDepth = GetBitDepth(stream), - }; - } - - private AudioStream ParseAudioStream(FFProbeStream stream) - { - return new AudioStream - { - Index = stream.Index, - BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseLongInvariant(stream.BitRate) : default, - CodecName = stream.CodecName, - CodecLongName = stream.CodecLongName, - CodecTag = stream.CodecTag, - CodecTagString = stream.CodecTagString, - Channels = stream.Channels ?? default, - ChannelLayout = stream.ChannelLayout, - Duration = MediaAnalysisUtils.ParseDuration(stream.Duration), - StartTime = MediaAnalysisUtils.ParseDuration(stream.StartTime), - SampleRateHz = !string.IsNullOrEmpty(stream.SampleRate) ? MediaAnalysisUtils.ParseIntInvariant(stream.SampleRate) : default, - Profile = stream.Profile, - Language = stream.GetLanguage(), - Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition), - Tags = stream.Tags.ToCaseInsensitive(), - BitDepth = GetBitDepth(stream), - }; - } - - private SubtitleStream ParseSubtitleStream(FFProbeStream stream) - { - return new SubtitleStream - { - Index = stream.Index, - BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseLongInvariant(stream.BitRate) : default, - CodecName = stream.CodecName, - CodecLongName = stream.CodecLongName, - Duration = MediaAnalysisUtils.ParseDuration(stream.Duration), - StartTime = MediaAnalysisUtils.ParseDuration(stream.StartTime), - Language = stream.GetLanguage(), - Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition), - Tags = stream.Tags.ToCaseInsensitive(), - }; - } + Format = ParseFormat(analysis.Format); + Chapters = analysis.Chapters.Select(c => ParseChapter(c)).ToList(); + VideoStreams = analysis.Streams.Where(stream => stream.CodecType == "video").Select(ParseVideoStream).ToList(); + AudioStreams = analysis.Streams.Where(stream => stream.CodecType == "audio").Select(ParseAudioStream).ToList(); + SubtitleStreams = analysis.Streams.Where(stream => stream.CodecType == "subtitle").Select(ParseSubtitleStream).ToList(); + ErrorData = analysis.ErrorData; } - public static class MediaAnalysisUtils + public TimeSpan Duration => new[] { Format.Duration, PrimaryVideoStream?.Duration ?? TimeSpan.Zero, PrimaryAudioStream?.Duration ?? TimeSpan.Zero }.Max(); + + public MediaFormat Format { get; } + + public List Chapters { get; } + + public AudioStream? PrimaryAudioStream => AudioStreams.OrderBy(stream => stream.Index).FirstOrDefault(); + public VideoStream? PrimaryVideoStream => VideoStreams.OrderBy(stream => stream.Index).FirstOrDefault(); + public SubtitleStream? PrimarySubtitleStream => SubtitleStreams.OrderBy(stream => stream.Index).FirstOrDefault(); + + public List VideoStreams { get; } + public List AudioStreams { get; } + public List SubtitleStreams { get; } + public IReadOnlyList ErrorData { get; } + + private MediaFormat ParseFormat(Format analysisFormat) { - private static readonly Regex DurationRegex = new(@"^(\d+):(\d{1,2}):(\d{1,2})\.(\d{1,3})", RegexOptions.Compiled); - - internal static Dictionary ToCaseInsensitive(this Dictionary? dictionary) + return new MediaFormat { - return dictionary?.ToDictionary(tag => tag.Key, tag => tag.Value, StringComparer.OrdinalIgnoreCase) ?? new Dictionary(); - } - public static double DivideRatio((double, double) ratio) => ratio.Item1 / ratio.Item2; + Duration = MediaAnalysisUtils.ParseDuration(analysisFormat.Duration), + StartTime = MediaAnalysisUtils.ParseDuration(analysisFormat.StartTime), + FormatName = analysisFormat.FormatName, + FormatLongName = analysisFormat.FormatLongName, + StreamCount = analysisFormat.NbStreams, + ProbeScore = analysisFormat.ProbeScore, + BitRate = long.Parse(analysisFormat.BitRate ?? "0"), + Tags = analysisFormat.Tags.ToCaseInsensitive() + }; + } - public static (int, int) ParseRatioInt(string input, char separator) + private string GetValue(string tagName, Dictionary? tags, string defaultValue) + { + return tags == null ? defaultValue : tags.TryGetValue(tagName, out var value) ? value : defaultValue; + } + + private ChapterData ParseChapter(Chapter analysisChapter) + { + var title = GetValue("title", analysisChapter.Tags, "TitleValueNotSet"); + var start = MediaAnalysisUtils.ParseDuration(analysisChapter.StartTime); + var end = MediaAnalysisUtils.ParseDuration(analysisChapter.EndTime); + + return new ChapterData(title, start, end); + } + + private int? GetBitDepth(FFProbeStream stream) + { + var bitDepth = int.TryParse(stream.BitsPerRawSample, out var bprs) ? bprs : stream.BitsPerSample; + return bitDepth == 0 ? null : bitDepth; + } + + private VideoStream ParseVideoStream(FFProbeStream stream) + { + return new VideoStream { - if (string.IsNullOrEmpty(input)) - { - return (0, 0); - } + Index = stream.Index, + AvgFrameRate = MediaAnalysisUtils.DivideRatio(MediaAnalysisUtils.ParseRatioDouble(stream.AvgFrameRate, '/')), + BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseLongInvariant(stream.BitRate) : default, + BitsPerRawSample = !string.IsNullOrEmpty(stream.BitsPerRawSample) ? MediaAnalysisUtils.ParseIntInvariant(stream.BitsPerRawSample) : default, + CodecName = stream.CodecName, + CodecLongName = stream.CodecLongName, + CodecTag = stream.CodecTag, + CodecTagString = stream.CodecTagString, + DisplayAspectRatio = MediaAnalysisUtils.ParseRatioInt(stream.DisplayAspectRatio, ':'), + SampleAspectRatio = MediaAnalysisUtils.ParseRatioInt(stream.SampleAspectRatio, ':'), + Duration = MediaAnalysisUtils.ParseDuration(stream.Duration), + StartTime = MediaAnalysisUtils.ParseDuration(stream.StartTime), + FrameRate = MediaAnalysisUtils.DivideRatio(MediaAnalysisUtils.ParseRatioDouble(stream.FrameRate, '/')), + Height = stream.Height ?? 0, + Width = stream.Width ?? 0, + Profile = stream.Profile, + PixelFormat = stream.PixelFormat, + Level = stream.Level, + ColorRange = stream.ColorRange, + ColorSpace = stream.ColorSpace, + ColorTransfer = stream.ColorTransfer, + ColorPrimaries = stream.ColorPrimaries, + Rotation = MediaAnalysisUtils.ParseRotation(stream), + Language = stream.GetLanguage(), + Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition), + Tags = stream.Tags.ToCaseInsensitive(), + BitDepth = GetBitDepth(stream) + }; + } - var ratio = input.Split(separator); - return (ParseIntInvariant(ratio[0]), ParseIntInvariant(ratio[1])); + private AudioStream ParseAudioStream(FFProbeStream stream) + { + return new AudioStream + { + Index = stream.Index, + BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseLongInvariant(stream.BitRate) : default, + CodecName = stream.CodecName, + CodecLongName = stream.CodecLongName, + CodecTag = stream.CodecTag, + CodecTagString = stream.CodecTagString, + Channels = stream.Channels ?? default, + ChannelLayout = stream.ChannelLayout, + Duration = MediaAnalysisUtils.ParseDuration(stream.Duration), + StartTime = MediaAnalysisUtils.ParseDuration(stream.StartTime), + SampleRateHz = !string.IsNullOrEmpty(stream.SampleRate) ? MediaAnalysisUtils.ParseIntInvariant(stream.SampleRate) : default, + Profile = stream.Profile, + Language = stream.GetLanguage(), + Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition), + Tags = stream.Tags.ToCaseInsensitive(), + BitDepth = GetBitDepth(stream) + }; + } + + private SubtitleStream ParseSubtitleStream(FFProbeStream stream) + { + return new SubtitleStream + { + Index = stream.Index, + BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseLongInvariant(stream.BitRate) : default, + CodecName = stream.CodecName, + CodecLongName = stream.CodecLongName, + Duration = MediaAnalysisUtils.ParseDuration(stream.Duration), + StartTime = MediaAnalysisUtils.ParseDuration(stream.StartTime), + Language = stream.GetLanguage(), + Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition), + Tags = stream.Tags.ToCaseInsensitive() + }; + } +} + +public static class MediaAnalysisUtils +{ + private static readonly Regex DurationRegex = new(@"^(\d+):(\d{1,2}):(\d{1,2})\.(\d{1,3})", RegexOptions.Compiled); + + internal static Dictionary ToCaseInsensitive(this Dictionary? dictionary) + { + return dictionary?.ToDictionary(tag => tag.Key, tag => tag.Value, StringComparer.OrdinalIgnoreCase) ?? new Dictionary(); + } + + public static double DivideRatio((double, double) ratio) + { + return ratio.Item1 / ratio.Item2; + } + + public static (int, int) ParseRatioInt(string input, char separator) + { + if (string.IsNullOrEmpty(input)) + { + return (0, 0); } - public static (double, double) ParseRatioDouble(string input, char separator) - { - if (string.IsNullOrEmpty(input)) - { - return (0, 0); - } + var ratio = input.Split(separator); + return (ParseIntInvariant(ratio[0]), ParseIntInvariant(ratio[1])); + } - var ratio = input.Split(separator); - return (ratio.Length > 0 ? ParseDoubleInvariant(ratio[0]) : 0, ratio.Length > 1 ? ParseDoubleInvariant(ratio[1]) : 0); + public static (double, double) ParseRatioDouble(string input, char separator) + { + if (string.IsNullOrEmpty(input)) + { + return (0, 0); } - public static double ParseDoubleInvariant(string line) => - double.Parse(line, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture); + var ratio = input.Split(separator); + return (ratio.Length > 0 ? ParseDoubleInvariant(ratio[0]) : 0, ratio.Length > 1 ? ParseDoubleInvariant(ratio[1]) : 0); + } - public static int ParseIntInvariant(string line) => - int.Parse(line, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture); + public static double ParseDoubleInvariant(string line) + { + return double.Parse(line, NumberStyles.Any, CultureInfo.InvariantCulture); + } - public static long ParseLongInvariant(string line) => - long.Parse(line, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture); + public static int ParseIntInvariant(string line) + { + return int.Parse(line, NumberStyles.Any, CultureInfo.InvariantCulture); + } - public static TimeSpan ParseDuration(string duration) + public static long ParseLongInvariant(string line) + { + return long.Parse(line, NumberStyles.Any, CultureInfo.InvariantCulture); + } + + public static TimeSpan ParseDuration(string duration) + { + if (!string.IsNullOrEmpty(duration)) { - if (!string.IsNullOrEmpty(duration)) + var match = DurationRegex.Match(duration); + if (match.Success) { - var match = DurationRegex.Match(duration); - if (match.Success) + // ffmpeg may provide < 3-digit number of milliseconds (omitting trailing zeros), which won't simply parse correctly + // e.g. 00:12:02.11 -> 12 minutes 2 seconds and 110 milliseconds + var millisecondsPart = match.Groups[4].Value; + if (millisecondsPart.Length < 3) { - // ffmpeg may provide < 3-digit number of milliseconds (omitting trailing zeros), which won't simply parse correctly - // e.g. 00:12:02.11 -> 12 minutes 2 seconds and 110 milliseconds - var millisecondsPart = match.Groups[4].Value; - if (millisecondsPart.Length < 3) - { - millisecondsPart = millisecondsPart.PadRight(3, '0'); - } + millisecondsPart = millisecondsPart.PadRight(3, '0'); + } - var hours = int.Parse(match.Groups[1].Value); - var minutes = int.Parse(match.Groups[2].Value); - var seconds = int.Parse(match.Groups[3].Value); - var milliseconds = int.Parse(millisecondsPart); - return new TimeSpan(0, hours, minutes, seconds, milliseconds); - } - else - { - return TimeSpan.Zero; - } - } - else - { - return TimeSpan.Zero; + var hours = int.Parse(match.Groups[1].Value); + var minutes = int.Parse(match.Groups[2].Value); + var seconds = int.Parse(match.Groups[3].Value); + var milliseconds = int.Parse(millisecondsPart); + return new TimeSpan(0, hours, minutes, seconds, milliseconds); } + + return TimeSpan.Zero; } - public static int ParseRotation(FFProbeStream fFProbeStream) - { - var displayMatrixSideData = fFProbeStream.SideData?.Find(item => item.TryGetValue("side_data_type", out var rawSideDataType) && rawSideDataType.ToString() == "Display Matrix"); + return TimeSpan.Zero; + } - if (displayMatrixSideData?.TryGetValue("rotation", out var rawRotation) ?? false) - { - return (int)float.Parse(rawRotation.ToString()); - } - else - { - return (int)float.Parse(fFProbeStream.GetRotate() ?? "0"); - } + public static int ParseRotation(FFProbeStream fFProbeStream) + { + var displayMatrixSideData = fFProbeStream.SideData?.Find(item => + item.TryGetValue("side_data_type", out var rawSideDataType) && rawSideDataType.ToString() == "Display Matrix"); + + if (displayMatrixSideData?.TryGetValue("rotation", out var rawRotation) ?? false) + { + return (int)float.Parse(rawRotation.ToString()); } - public static Dictionary? FormatDisposition(Dictionary? disposition) + return (int)float.Parse(fFProbeStream.GetRotate() ?? "0"); + } + + public static Dictionary? FormatDisposition(Dictionary? disposition) + { + if (disposition == null) { - if (disposition == null) - { - return null; - } + return null; + } - var result = new Dictionary(disposition.Count, StringComparer.Ordinal); + var result = new Dictionary(disposition.Count, StringComparer.Ordinal); - foreach (var pair in disposition) - { - result.Add(pair.Key, ToBool(pair.Value)); - } + foreach (var pair in disposition) + { + result.Add(pair.Key, ToBool(pair.Value)); + } - static bool ToBool(int value) => value switch + static bool ToBool(int value) + { + return value switch { 0 => false, 1 => true, _ => throw new ArgumentOutOfRangeException(nameof(value), $"Not expected disposition state value: {value}") }; - - return result; } + + return result; } } diff --git a/FFMpegCore/FFProbe/MediaFormat.cs b/FFMpegCore/FFProbe/MediaFormat.cs index 7269a95..08bbd99 100644 --- a/FFMpegCore/FFProbe/MediaFormat.cs +++ b/FFMpegCore/FFProbe/MediaFormat.cs @@ -1,14 +1,13 @@ -namespace FFMpegCore +namespace FFMpegCore; + +public class MediaFormat : ITagsContainer { - public class MediaFormat : ITagsContainer - { - public TimeSpan Duration { get; set; } - public TimeSpan StartTime { get; set; } - public string FormatName { get; set; } = null!; - public string FormatLongName { get; set; } = null!; - public int StreamCount { get; set; } - public double ProbeScore { get; set; } - public double BitRate { get; set; } - public Dictionary? Tags { get; set; } - } + public TimeSpan Duration { get; set; } + public TimeSpan StartTime { get; set; } + public string FormatName { get; set; } = null!; + public string FormatLongName { get; set; } = null!; + public int StreamCount { get; set; } + public double ProbeScore { get; set; } + public double BitRate { get; set; } + public Dictionary? Tags { get; set; } } diff --git a/FFMpegCore/FFProbe/MediaStream.cs b/FFMpegCore/FFProbe/MediaStream.cs index 48d1f38..d73de4b 100644 --- a/FFMpegCore/FFProbe/MediaStream.cs +++ b/FFMpegCore/FFProbe/MediaStream.cs @@ -1,22 +1,24 @@ using FFMpegCore.Enums; -namespace FFMpegCore -{ - public abstract class MediaStream : ITagsContainer - { - public int Index { get; set; } - public string CodecName { get; set; } = null!; - public string CodecLongName { get; set; } = null!; - public string CodecTagString { get; set; } = null!; - public string CodecTag { get; set; } = null!; - public long BitRate { get; set; } - public TimeSpan StartTime { get; set; } - public TimeSpan Duration { get; set; } - public string? Language { get; set; } - public Dictionary? Disposition { get; set; } - public Dictionary? Tags { get; set; } - public int? BitDepth { get; set; } +namespace FFMpegCore; - public Codec GetCodecInfo() => FFMpeg.GetCodec(CodecName); +public abstract class MediaStream : ITagsContainer +{ + public int Index { get; set; } + public string CodecName { get; set; } = null!; + public string CodecLongName { get; set; } = null!; + public string CodecTagString { get; set; } = null!; + public string CodecTag { get; set; } = null!; + public long BitRate { get; set; } + public TimeSpan StartTime { get; set; } + public TimeSpan Duration { get; set; } + public string? Language { get; set; } + public Dictionary? Disposition { get; set; } + public int? BitDepth { get; set; } + public Dictionary? Tags { get; set; } + + public Codec GetCodecInfo() + { + return FFMpeg.GetCodec(CodecName); } } diff --git a/FFMpegCore/FFProbe/PacketAnalysis.cs b/FFMpegCore/FFProbe/PacketAnalysis.cs index 38ecd13..f00240e 100644 --- a/FFMpegCore/FFProbe/PacketAnalysis.cs +++ b/FFMpegCore/FFProbe/PacketAnalysis.cs @@ -1,46 +1,33 @@ using System.Text.Json.Serialization; -namespace FFMpegCore +namespace FFMpegCore; + +public class FFProbePacketAnalysis { - public class FFProbePacketAnalysis - { - [JsonPropertyName("codec_type")] - public string CodecType { get; set; } = null!; + [JsonPropertyName("codec_type")] public string CodecType { get; set; } = null!; - [JsonPropertyName("stream_index")] - public int StreamIndex { get; set; } + [JsonPropertyName("stream_index")] public int StreamIndex { get; set; } - [JsonPropertyName("pts")] - public long Pts { get; set; } + [JsonPropertyName("pts")] public long Pts { get; set; } - [JsonPropertyName("pts_time")] - public string PtsTime { get; set; } = null!; + [JsonPropertyName("pts_time")] public string PtsTime { get; set; } = null!; - [JsonPropertyName("dts")] - public long Dts { get; set; } + [JsonPropertyName("dts")] public long Dts { get; set; } - [JsonPropertyName("dts_time")] - public string DtsTime { get; set; } = null!; + [JsonPropertyName("dts_time")] public string DtsTime { get; set; } = null!; - [JsonPropertyName("duration")] - public int Duration { get; set; } + [JsonPropertyName("duration")] public int Duration { get; set; } - [JsonPropertyName("duration_time")] - public string DurationTime { get; set; } = null!; + [JsonPropertyName("duration_time")] public string DurationTime { get; set; } = null!; - [JsonPropertyName("size")] - public int Size { get; set; } + [JsonPropertyName("size")] public int Size { get; set; } - [JsonPropertyName("pos")] - public long Pos { get; set; } + [JsonPropertyName("pos")] public long Pos { get; set; } - [JsonPropertyName("flags")] - public string Flags { get; set; } = null!; - } - - public class FFProbePackets - { - [JsonPropertyName("packets")] - public List Packets { get; set; } = null!; - } + [JsonPropertyName("flags")] public string Flags { get; set; } = null!; +} + +public class FFProbePackets +{ + [JsonPropertyName("packets")] public List Packets { get; set; } = null!; } diff --git a/FFMpegCore/FFProbe/ProcessArgumentsExtensions.cs b/FFMpegCore/FFProbe/ProcessArgumentsExtensions.cs index 47da20d..ef7140a 100644 --- a/FFMpegCore/FFProbe/ProcessArgumentsExtensions.cs +++ b/FFMpegCore/FFProbe/ProcessArgumentsExtensions.cs @@ -1,18 +1,18 @@ using Instances; -namespace FFMpegCore +namespace FFMpegCore; + +public static class ProcessArgumentsExtensions { - public static class ProcessArgumentsExtensions + public static IProcessResult StartAndWaitForExit(this ProcessArguments processArguments) { - public static IProcessResult StartAndWaitForExit(this ProcessArguments processArguments) - { - using var instance = processArguments.Start(); - return instance.WaitForExit(); - } - public static async Task StartAndWaitForExitAsync(this ProcessArguments processArguments, CancellationToken cancellationToken = default) - { - using var instance = processArguments.Start(); - return await instance.WaitForExitAsync(cancellationToken); - } + using var instance = processArguments.Start(); + return instance.WaitForExit(); + } + + public static async Task StartAndWaitForExitAsync(this ProcessArguments processArguments, CancellationToken cancellationToken = default) + { + using var instance = processArguments.Start(); + return await instance.WaitForExitAsync(cancellationToken); } } diff --git a/FFMpegCore/FFProbe/SubtitleStream.cs b/FFMpegCore/FFProbe/SubtitleStream.cs index 83f02c8..0ee0022 100644 --- a/FFMpegCore/FFProbe/SubtitleStream.cs +++ b/FFMpegCore/FFProbe/SubtitleStream.cs @@ -1,7 +1,5 @@ -namespace FFMpegCore -{ - public class SubtitleStream : MediaStream - { +namespace FFMpegCore; - } +public class SubtitleStream : MediaStream +{ } diff --git a/FFMpegCore/FFProbe/VideoStream.cs b/FFMpegCore/FFProbe/VideoStream.cs index 377b2d0..02b7b12 100644 --- a/FFMpegCore/FFProbe/VideoStream.cs +++ b/FFMpegCore/FFProbe/VideoStream.cs @@ -1,26 +1,28 @@ using FFMpegCore.Enums; -namespace FFMpegCore -{ - public class VideoStream : MediaStream - { - public double AvgFrameRate { get; set; } - public int BitsPerRawSample { get; set; } - public (int Width, int Height) DisplayAspectRatio { get; set; } - public (int Width, int Height) SampleAspectRatio { get; set; } - public string Profile { get; set; } = null!; - public int Width { get; set; } - public int Height { get; set; } - public double FrameRate { get; set; } - public string PixelFormat { get; set; } = null!; - public int Level { get; set; } - public int Rotation { get; set; } - public double AverageFrameRate { get; set; } - public string ColorRange { get; set; } = null!; - public string ColorSpace { get; set; } = null!; - public string ColorTransfer { get; set; } = null!; - public string ColorPrimaries { get; set; } = null!; +namespace FFMpegCore; - public PixelFormat GetPixelFormatInfo() => FFMpeg.GetPixelFormat(PixelFormat); +public class VideoStream : MediaStream +{ + public double AvgFrameRate { get; set; } + public int BitsPerRawSample { get; set; } + public (int Width, int Height) DisplayAspectRatio { get; set; } + public (int Width, int Height) SampleAspectRatio { get; set; } + public string Profile { get; set; } = null!; + public int Width { get; set; } + public int Height { get; set; } + public double FrameRate { get; set; } + public string PixelFormat { get; set; } = null!; + public int Level { get; set; } + public int Rotation { get; set; } + public double AverageFrameRate { get; set; } + public string ColorRange { get; set; } = null!; + public string ColorSpace { get; set; } = null!; + public string ColorTransfer { get; set; } = null!; + public string ColorPrimaries { get; set; } = null!; + + public PixelFormat GetPixelFormatInfo() + { + return FFMpeg.GetPixelFormat(PixelFormat); } } diff --git a/FFMpegCore/GlobalFFOptions.cs b/FFMpegCore/GlobalFFOptions.cs index 49cac16..a4c48ae 100644 --- a/FFMpegCore/GlobalFFOptions.cs +++ b/FFMpegCore/GlobalFFOptions.cs @@ -1,59 +1,63 @@ using System.Runtime.InteropServices; using System.Text.Json; -namespace FFMpegCore +namespace FFMpegCore; + +public static class GlobalFFOptions { - public static class GlobalFFOptions + private const string ConfigFile = "ffmpeg.config.json"; + private static FFOptions? _current; + + public static FFOptions Current => _current ??= LoadFFOptions(); + + public static void Configure(Action optionsAction) { - private const string ConfigFile = "ffmpeg.config.json"; - private static FFOptions? _current; + optionsAction.Invoke(Current); + } - public static FFOptions Current => _current ??= LoadFFOptions(); + public static void Configure(FFOptions ffOptions) + { + _current = ffOptions ?? throw new ArgumentNullException(nameof(ffOptions)); + } - public static void Configure(Action optionsAction) => optionsAction.Invoke(Current); + public static string GetFFMpegBinaryPath(FFOptions? ffOptions = null) + { + return GetFFBinaryPath("FFMpeg", ffOptions ?? Current); + } - public static void Configure(FFOptions ffOptions) + public static string GetFFProbeBinaryPath(FFOptions? ffOptions = null) + { + return GetFFBinaryPath("FFProbe", ffOptions ?? Current); + } + + private static string GetFFBinaryPath(string name, FFOptions ffOptions) + { + var ffName = name.ToLowerInvariant(); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - _current = ffOptions ?? throw new ArgumentNullException(nameof(ffOptions)); + ffName += ".exe"; } - public static string GetFFMpegBinaryPath(FFOptions? ffOptions = null) => GetFFBinaryPath("FFMpeg", ffOptions ?? Current); + var target = Environment.Is64BitProcess ? "x64" : "x86"; + var possiblePaths = new List { Path.Combine(ffOptions.BinaryFolder, target), ffOptions.BinaryFolder }; - public static string GetFFProbeBinaryPath(FFOptions? ffOptions = null) => GetFFBinaryPath("FFProbe", ffOptions ?? Current); - - private static string GetFFBinaryPath(string name, FFOptions ffOptions) + foreach (var possiblePath in possiblePaths) { - var ffName = name.ToLowerInvariant(); - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + var possibleFFMpegPath = Path.Combine(possiblePath, ffName); + if (File.Exists(possibleFFMpegPath)) { - ffName += ".exe"; + return possibleFFMpegPath; } - - var target = Environment.Is64BitProcess ? "x64" : "x86"; - var possiblePaths = new List() - { - Path.Combine(ffOptions.BinaryFolder, target), - ffOptions.BinaryFolder - }; - - foreach (var possiblePath in possiblePaths) - { - var possibleFFMpegPath = Path.Combine(possiblePath, ffName); - if (File.Exists(possibleFFMpegPath)) - { - return possibleFFMpegPath; - } - } - - //Fall back to the assumption this tool exists in the PATH - return ffName; } - private static FFOptions LoadFFOptions() - { - return File.Exists(ConfigFile) - ? JsonSerializer.Deserialize(File.ReadAllText(ConfigFile))! - : new FFOptions(); - } + //Fall back to the assumption this tool exists in the PATH + return ffName; + } + + private static FFOptions LoadFFOptions() + { + return File.Exists(ConfigFile) + ? JsonSerializer.Deserialize(File.ReadAllText(ConfigFile))! + : new FFOptions(); } } diff --git a/FFMpegCore/Helpers/FFMpegHelper.cs b/FFMpegCore/Helpers/FFMpegHelper.cs index 3fb03eb..11540c9 100644 --- a/FFMpegCore/Helpers/FFMpegHelper.cs +++ b/FFMpegCore/Helpers/FFMpegHelper.cs @@ -1,53 +1,54 @@ using FFMpegCore.Exceptions; using Instances; -namespace FFMpegCore.Helpers +namespace FFMpegCore.Helpers; + +public static class FFMpegHelper { - public static class FFMpegHelper + private static bool _ffmpegVerified; + + public static void ConversionSizeExceptionCheck(IMediaAnalysis info) { - private static bool _ffmpegVerified; + ConversionSizeExceptionCheck(info.PrimaryVideoStream!.Width, info.PrimaryVideoStream.Height); + } - public static void ConversionSizeExceptionCheck(IMediaAnalysis info) - => ConversionSizeExceptionCheck(info.PrimaryVideoStream!.Width, info.PrimaryVideoStream.Height); - - public static void ConversionSizeExceptionCheck(int width, int height) + public static void ConversionSizeExceptionCheck(int width, int height) + { + if (height % 2 != 0 || width % 2 != 0) { - if (height % 2 != 0 || width % 2 != 0) - { - throw new ArgumentException("FFMpeg yuv420p encoding requires the width and height to be a multiple of 2!"); - } + throw new ArgumentException("FFMpeg yuv420p encoding requires the width and height to be a multiple of 2!"); + } + } + + public static void ExtensionExceptionCheck(string filename, string extension) + { + if (!extension.Equals(Path.GetExtension(filename), StringComparison.OrdinalIgnoreCase)) + { + throw new FFMpegException(FFMpegExceptionType.File, + $"Invalid output file. File extension should be '{extension}' required."); + } + } + + public static void RootExceptionCheck() + { + if (GlobalFFOptions.Current.BinaryFolder == null) + { + throw new FFOptionsException("FFMpeg root is not configured in app config. Missing key 'BinaryFolder'."); + } + } + + public static void VerifyFFMpegExists(FFOptions ffMpegOptions) + { + if (_ffmpegVerified) + { + return; } - public static void ExtensionExceptionCheck(string filename, string extension) + var result = Instance.Finish(GlobalFFOptions.GetFFMpegBinaryPath(ffMpegOptions), "-version"); + _ffmpegVerified = result.ExitCode == 0; + if (!_ffmpegVerified) { - if (!extension.Equals(Path.GetExtension(filename), StringComparison.OrdinalIgnoreCase)) - { - throw new FFMpegException(FFMpegExceptionType.File, - $"Invalid output file. File extension should be '{extension}' required."); - } - } - - public static void RootExceptionCheck() - { - if (GlobalFFOptions.Current.BinaryFolder == null) - { - throw new FFOptionsException("FFMpeg root is not configured in app config. Missing key 'BinaryFolder'."); - } - } - - public static void VerifyFFMpegExists(FFOptions ffMpegOptions) - { - if (_ffmpegVerified) - { - return; - } - - var result = Instance.Finish(GlobalFFOptions.GetFFMpegBinaryPath(ffMpegOptions), "-version"); - _ffmpegVerified = result.ExitCode == 0; - if (!_ffmpegVerified) - { - throw new FFMpegException(FFMpegExceptionType.Operation, "ffmpeg was not found on your system"); - } + throw new FFMpegException(FFMpegExceptionType.Operation, "ffmpeg was not found on your system"); } } } diff --git a/FFMpegCore/Helpers/FFProbeHelper.cs b/FFMpegCore/Helpers/FFProbeHelper.cs index ff1ff20..307290e 100644 --- a/FFMpegCore/Helpers/FFProbeHelper.cs +++ b/FFMpegCore/Helpers/FFProbeHelper.cs @@ -1,33 +1,32 @@ using FFMpegCore.Exceptions; using Instances; -namespace FFMpegCore.Helpers -{ - public static class FFProbeHelper - { - private static bool _ffprobeVerified; +namespace FFMpegCore.Helpers; - public static void RootExceptionCheck() +public static class FFProbeHelper +{ + private static bool _ffprobeVerified; + + public static void RootExceptionCheck() + { + if (GlobalFFOptions.Current.BinaryFolder == null) { - if (GlobalFFOptions.Current.BinaryFolder == null) - { - throw new FFOptionsException("FFProbe root is not configured in app config. Missing key 'BinaryFolder'."); - } + throw new FFOptionsException("FFProbe root is not configured in app config. Missing key 'BinaryFolder'."); + } + } + + public static void VerifyFFProbeExists(FFOptions ffMpegOptions) + { + if (_ffprobeVerified) + { + return; } - public static void VerifyFFProbeExists(FFOptions ffMpegOptions) + var result = Instance.Finish(GlobalFFOptions.GetFFProbeBinaryPath(ffMpegOptions), "-version"); + _ffprobeVerified = result.ExitCode == 0; + if (!_ffprobeVerified) { - if (_ffprobeVerified) - { - return; - } - - var result = Instance.Finish(GlobalFFOptions.GetFFProbeBinaryPath(ffMpegOptions), "-version"); - _ffprobeVerified = result.ExitCode == 0; - if (!_ffprobeVerified) - { - throw new FFProbeException("ffprobe was not found on your system"); - } + throw new FFProbeException("ffprobe was not found on your system"); } } } diff --git a/README.md b/README.md index 33f7ddf..dcee337 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,34 @@ -# [FFMpegCore](https://www.nuget.org/packages/FFMpegCore/) -[![NuGet Badge](https://buildstats.info/nuget/FFMpegCore)](https://www.nuget.org/packages/FFMpegCore/) +# [FFMpegCore](https://www.nuget.org/packages/FFMpegCore/) + +[![NuGet Version](https://img.shields.io/nuget/v/FFMpegCore)](https://www.nuget.org/packages/FFMpegCore/) [![GitHub issues](https://img.shields.io/github/issues/rosenbjerg/FFMpegCore)](https://github.com/rosenbjerg/FFMpegCore/issues) [![GitHub stars](https://img.shields.io/github/stars/rosenbjerg/FFMpegCore)](https://github.com/rosenbjerg/FFMpegCore/stargazers) [![GitHub](https://img.shields.io/github/license/rosenbjerg/FFMpegCore)](https://github.com/rosenbjerg/FFMpegCore/blob/master/LICENSE) +[![codecov](https://codecov.io/gh/rosenbjerg/FFMpegCore/branch/main/graph/badge.svg)](https://codecov.io/gh/rosenbjerg/FFMpegCore) [![CI](https://github.com/rosenbjerg/FFMpegCore/workflows/CI/badge.svg)](https://github.com/rosenbjerg/FFMpegCore/actions/workflows/ci.yml) [![GitHub code contributors](https://img.shields.io/github/contributors/rosenbjerg/FFMpegCore)](https://github.com/rosenbjerg/FFMpegCore/graphs/contributors) -A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications. Supports both synchronous and asynchronous calls +A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications. Supports both +synchronous and asynchronous calls # API ## FFProbe + Use FFProbe to analyze media files: ```csharp var mediaInfo = await FFProbe.AnalyseAsync(inputPath); ``` -or + +or + ```csharp var mediaInfo = FFProbe.Analyse(inputPath); ``` ## FFMpeg + Use FFMpeg to convert your media files. Easily build your FFMpeg arguments using the fluent argument builder: @@ -42,6 +49,7 @@ FFMpegArguments ``` Convert to and/or from streams + ```csharp await FFMpegArguments .FromPipeInput(new StreamPipeSource(inputStream)) @@ -52,9 +60,11 @@ await FFMpegArguments ``` ## Helper methods + The provided helper methods makes it simple to perform common operations. ### Easily capture snapshots from a video file: + ```csharp // process the snapshot in-memory and use the Bitmap directly var bitmap = FFMpeg.Snapshot(inputPath, new Size(200, 400), TimeSpan.FromMinutes(1)); @@ -64,6 +74,7 @@ FFMpeg.Snapshot(inputPath, outputPath, new Size(200, 400), TimeSpan.FromMinutes( ``` ### You can also capture GIF snapshots from a video file: + ```csharp FFMpeg.GifSnapshot(inputPath, outputPath, new Size(200, 400), TimeSpan.FromSeconds(10)); @@ -75,6 +86,7 @@ await FFMpeg.GifSnapshotAsync(inputPath, outputPath, new Size(480, -1), TimeSpan ``` ### Join video parts into one single file: + ```csharp FFMpeg.Join(@"..\joined_video.mp4", @"..\part1.mp4", @@ -84,6 +96,7 @@ FFMpeg.Join(@"..\joined_video.mp4", ``` ### Create a sub video + ``` csharp FFMpeg.SubVideo(inputPath, outputPath, @@ -93,6 +106,7 @@ FFMpeg.SubVideo(inputPath, ``` ### Join images into a video: + ```csharp FFMpeg.JoinImageSequence(@"..\joined_video.mp4", frameRate: 1, ImageInfo.FromPath(@"..\1.png"), @@ -102,21 +116,25 @@ FFMpeg.JoinImageSequence(@"..\joined_video.mp4", frameRate: 1, ``` ### Mute the audio of a video file: + ```csharp FFMpeg.Mute(inputPath, outputPath); ``` ### Extract the audio track from a video file: + ```csharp FFMpeg.ExtractAudio(inputPath, outputPath); ``` ### Add or replace the audio track of a video file: + ```csharp FFMpeg.ReplaceAudio(inputPath, inputAudioPath, outputPath); ``` ### Combine an image with audio file, for youtube or similar platforms + ```csharp FFMpeg.PosterWithAudio(inputPath, inputAudioPath, outputPath); // or @@ -127,13 +145,17 @@ image.AddAudio(inputAudioPath, outputPath); Other available arguments could be found in `FFMpegCore.Arguments` namespace. ## Input piping -With input piping it is possible to write video frames directly from program memory without saving them to jpeg or png and then passing path to input of ffmpeg. This feature also allows for converting video on-the-fly while frames are being generated or received. -An object implementing the `IPipeSource` interface is used as the source of data. Currently, the `IPipeSource` interface has two implementations; `StreamPipeSource` for streams, and `RawVideoPipeSource` for raw video frames. +With input piping it is possible to write video frames directly from program memory without saving them to jpeg or png and then passing path +to input of ffmpeg. This feature also allows for converting video on-the-fly while frames are being generated or received. + +An object implementing the `IPipeSource` interface is used as the source of data. Currently, the `IPipeSource` interface has two +implementations; `StreamPipeSource` for streams, and `RawVideoPipeSource` for raw video frames. ### Working with raw video frames Method for generating bitmap frames: + ```csharp IEnumerable CreateFrames(int count) { @@ -145,6 +167,7 @@ IEnumerable CreateFrames(int count) ``` Then create a `RawVideoPipeSource` that utilises your video frame source + ```csharp var videoFramesSource = new RawVideoPipeSource(CreateFrames(64)) { @@ -159,28 +182,36 @@ await FFMpegArguments If you want to use `System.Drawing.Bitmap`s as `IVideoFrame`s, a `BitmapVideoFrameWrapper` wrapper class is provided. - # Binaries -## Installation -If you prefer to manually download them, visit [ffbinaries](https://ffbinaries.com/downloads) or [zeranoe Windows builds](https://ffmpeg.zeranoe.com/builds/). +## Runtime Auto Installation +You can install a version of ffmpeg suite at runtime using `FFMpegDownloader.DownloadFFMpegSuite();` + +This feature uses the api from [ffbinaries](https://ffbinaries.com/api). + +## Manual Installation + +If you prefer to manually download them, visit [ffbinaries](https://ffbinaries.com/downloads) +or [zeranoe Windows builds](https://ffmpeg.zeranoe.com/builds/). ### Windows (using choco) + command: `choco install ffmpeg -y` location: `C:\ProgramData\chocolatey\lib\ffmpeg\tools\ffmpeg\bin` ### Mac OSX + command: `brew install ffmpeg mono-libgdiplus` location: `/usr/local/bin` ### Ubuntu + command: `sudo apt-get install -y ffmpeg libgdiplus` location: `/usr/bin` - ## Path Configuration ### Option 1 @@ -216,7 +247,8 @@ await FFMpegArguments ### Option 2 -The root and temp directory for the ffmpeg binaries can be configured via the `ffmpeg.config.json` file, which will be read on first use only. +The root and temp directory for the ffmpeg binaries can be configured via the `ffmpeg.config.json` file, which will be read on first use +only. ```json { @@ -226,8 +258,10 @@ The root and temp directory for the ffmpeg binaries can be configured via the `f ``` ### Supporting both 32 and 64 bit processes -If you wish to support multiple client processor architectures, you can do so by creating two folders, `x64` and `x86`, in the `BinaryFolder` directory. -Both folders should contain the binaries (`ffmpeg.exe` and `ffprobe.exe`) built for the respective architectures. + +If you wish to support multiple client processor architectures, you can do so by creating two folders, `x64` and `x86`, in the +`BinaryFolder` directory. +Both folders should contain the binaries (`ffmpeg.exe` and `ffprobe.exe`) built for the respective architectures. By doing so, the library will attempt to use either `/{BinaryFolder}/{ARCH}/(ffmpeg|ffprobe).exe`. @@ -235,19 +269,20 @@ If these folders are not defined, it will try to find the binaries in `/{BinaryF (`.exe` is only appended on Windows) - # Compatibility -Older versions of ffmpeg might not support all ffmpeg arguments available through this library. The library has been tested with version `3.3` to `4.2` +Older versions of ffmpeg might not support all ffmpeg arguments available through this library. The library has been tested with version +`3.3` to `4.2` ## Code contributors + ## Other contributors - + ### License