diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c4fdd36 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,275 @@ +# EditorConfig is awesome:http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Don't use tabs for indentation. +[*] +indent_style = space +# (Please don't specify an indent_size here; that has too many unintended consequences.) + +# Code files +[*.{cs,csx,vb,vbx}] +indent_size = 4 +insert_final_newline = true +charset = utf-8-bom + +# Xml project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# Xml config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + +# JSON files +[*.json] +indent_size = 2 + +[*.{sh}] +end_of_line = lf +indent_size = 2 + +# Dotnet code style settings: +[*.{cs,vb}] + +# Sort using and Import directives with System.* appearing first +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = false + +# Avoid "this." and "Me." if not necessary +dotnet_style_qualification_for_field = false:error +dotnet_style_qualification_for_property = false:error +dotnet_style_qualification_for_method = false:error +dotnet_style_qualification_for_event = false:error + +# Use language keywords instead of framework type names for type references +dotnet_style_predefined_type_for_locals_parameters_members = true:error +dotnet_style_predefined_type_for_member_access = true:error + +# Suggest more modern language features when available +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion + +# Whitespace options +dotnet_style_allow_multiple_blank_lines_experimental = false +dotnet_style_allow_statement_immediately_after_block_experimental = false + +# Non-private static fields are PascalCase +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields +dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style + +dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field +dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected +dotnet_naming_symbols.non_private_static_fields.required_modifiers = static + +dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case + +# Non-private readonly fields are PascalCase +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields +dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = non_private_readonly_field_style + +dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field +dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected +dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly + +dotnet_naming_style.non_private_readonly_field_style.capitalization = pascal_case + +# Constants are PascalCase +dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants +dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style + +dotnet_naming_symbols.constants.applicable_kinds = field, local +dotnet_naming_symbols.constants.required_modifiers = const + +dotnet_naming_style.constant_style.capitalization = pascal_case + +# Static fields are camelCase and start with s_ +dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion +dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields +dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style + +dotnet_naming_symbols.static_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.required_modifiers = static + +dotnet_naming_style.static_field_style.capitalization = camel_case +dotnet_naming_style.static_field_style.required_prefix = _ + +# Instance fields are camelCase and start with _ +dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion +dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields +dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style + +dotnet_naming_symbols.instance_fields.applicable_kinds = field + +dotnet_naming_style.instance_field_style.capitalization = camel_case +dotnet_naming_style.instance_field_style.required_prefix = _ + +# Locals and parameters are camelCase +dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion +dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters +dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style + +dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local + +dotnet_naming_style.camel_case_style.capitalization = camel_case + +# Local functions are PascalCase +dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions +dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style + +dotnet_naming_symbols.local_functions.applicable_kinds = local_function + +dotnet_naming_style.local_function_style.capitalization = pascal_case + +# By default, name items with PascalCase +dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members +dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style + +dotnet_naming_symbols.all_members.applicable_kinds = * + +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# IDE0073: File header +dotnet_diagnostic.IDE0073.severity = warning + +# IDE0035: Remove unreachable code +dotnet_diagnostic.IDE0035.severity = warning + +# IDE0036: Order modifiers +dotnet_diagnostic.IDE0036.severity = warning + +# IDE0043: Format string contains invalid placeholder +dotnet_diagnostic.IDE0043.severity = warning + +# IDE0044: Make field readonly +dotnet_diagnostic.IDE0044.severity = warning + +# IDE0011: Add braces +csharp_prefer_braces = when_multiline:warning +# NOTE: We need the below severity entry for Add Braces due to https://github.com/dotnet/roslyn/issues/44201 +dotnet_diagnostic.IDE0011.severity = warning + +# IDE0040: Add accessibility modifiers +dotnet_diagnostic.IDE0040.severity = warning + +# CONSIDER: Are IDE0051 and IDE0052 too noisy to be warnings for IDE editing scenarios? Should they be made build-only warnings? +# IDE0051: Remove unused private member +dotnet_diagnostic.IDE0051.severity = warning + +# IDE0052: Remove unread private member +dotnet_diagnostic.IDE0052.severity = warning + +# IDE0059: Unnecessary assignment to a value +dotnet_diagnostic.IDE0059.severity = warning + +# IDE0060: Remove unused parameter +dotnet_diagnostic.IDE0060.severity = warning + +# CA1012: Abstract types should not have public constructors +dotnet_diagnostic.CA1012.severity = warning + +# CA1822: Make member static +dotnet_diagnostic.CA1822.severity = warning + +# IDE0005: Using directive is unnecessary +dotnet_diagnostic.IDE0005.severity = warning + +# dotnet_style_allow_multiple_blank_lines_experimental +dotnet_diagnostic.IDE2000.severity = warning + +# csharp_style_allow_embedded_statements_on_same_line_experimental +dotnet_diagnostic.IDE2001.severity = warning + +# csharp_style_allow_blank_lines_between_consecutive_braces_experimental +dotnet_diagnostic.IDE2002.severity = warning + +# dotnet_style_allow_statement_immediately_after_block_experimental +dotnet_diagnostic.IDE2003.severity = warning + +# csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental +dotnet_diagnostic.IDE2004.severity = warning + +# CSharp code style settings: +[*.cs] +# Newline settings +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left + +# Whitespace options +csharp_style_allow_embedded_statements_on_same_line_experimental = false +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false + +# Prefer "var" everywhere +dotnet_diagnostic.IDE0007.severity = error +csharp_style_var_for_built_in_types = true:error +csharp_style_var_when_type_is_apparent = true:error +csharp_style_var_elsewhere = true:error + +# Prefer method-like constructs to have a block body +csharp_style_expression_bodied_methods = false:none +csharp_style_expression_bodied_constructors = false:none +csharp_style_expression_bodied_operators = false:none + +# Prefer property-like constructs to have an expression-body +csharp_style_expression_bodied_properties = true:error +csharp_style_expression_bodied_indexers = true:error +csharp_style_expression_bodied_accessors = true:error + +# Suggest more modern language features when available +csharp_style_pattern_matching_over_is_with_cast_check = true:error +csharp_style_pattern_matching_over_as_with_null_check = true:error +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_throw_expression = true:error +csharp_style_conditional_delegate_call = true:suggestion + +# Spacing +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = do_not_ignore +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Blocks are allowed +csharp_prefer_braces = true:silent +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5afb8d4..c6d3529 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,9 +5,9 @@ on: branches: - master paths: - - .github/workflows/ci.yml - - FFMpegCore/** - - FFMpegCore.Test/** + - .github/workflows/ci.yml + - FFMpegCore/** + - FFMpegCore.Test/** pull_request: branches: - master @@ -22,16 +22,31 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [windows-latest, ubuntu-latest] - timeout-minutes: 6 + os: [windows-latest, ubuntu-latest, macos-latest] + timeout-minutes: 7 steps: + - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 + - name: Prepare .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v3 with: - dotnet-version: '6.0.x' + dotnet-version: '7.0.x' + + - name: Lint with dotnet + run: dotnet format FFMpegCore.sln --severity warn --verify-no-changes + - name: Prepare FFMpeg - uses: FedericoCarboni/setup-ffmpeg@v1 + uses: FedericoCarboni/setup-ffmpeg@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + - name: Test with dotnet - run: dotnet test --logger GitHubActions + run: dotnet test FFMpegCore.sln --collect "XPlat Code Coverage" --logger GitHubActions + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 + with: + directory: FFMpegCore.Test/TestResults + fail_ci_if_error: true \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7cf1425..0c7725b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,13 +8,16 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 - - name: Prepare .NET - uses: actions/setup-dotnet@v1 - with: - dotnet-version: '6.0.x' - - name: Build solution - run: dotnet build --output build -c Release - - name: Publish NuGet package - run: dotnet nuget push "build/*.nupkg" --source nuget.org --api-key ${{ secrets.NUGET_TOKEN }} + uses: actions/checkout@v3 + + - name: Prepare .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '7.0.x' + + - name: Build solution + run: dotnet pack FFMpegCore.sln --output build -c Release + + - name: Publish NuGet package + run: dotnet nuget push build/*.nupkg --source nuget.org --api-key ${{ secrets.NUGET_TOKEN }} diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..2944c6b --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,17 @@ + + + netstandard2.0 + en + 5.0.0.0 + default + enable + true + enable + + GitHub + https://github.com/rosenbjerg/FFMpegCore + https://github.com/rosenbjerg/FFMpegCore + MIT + en + + \ No newline at end of file diff --git a/FFMpegCore.Examples/FFMpegCore.Examples.csproj b/FFMpegCore.Examples/FFMpegCore.Examples.csproj index 68e7b5c..347607f 100644 --- a/FFMpegCore.Examples/FFMpegCore.Examples.csproj +++ b/FFMpegCore.Examples/FFMpegCore.Examples.csproj @@ -6,6 +6,7 @@ + diff --git a/FFMpegCore.Examples/Program.cs b/FFMpegCore.Examples/Program.cs index a718a21..ac4bce5 100644 --- a/FFMpegCore.Examples/Program.cs +++ b/FFMpegCore.Examples/Program.cs @@ -1,11 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.IO; +using System.Drawing; using FFMpegCore; using FFMpegCore.Enums; +using FFMpegCore.Extensions.System.Drawing.Common; using FFMpegCore.Pipes; -using FFMpegCore.Extend; var inputPath = "/path/to/input"; var outputPath = "/path/to/output"; @@ -34,7 +31,7 @@ { // process the snapshot in-memory and use the Bitmap directly - var bitmap = FFMpeg.Snapshot(inputPath, new Size(200, 400), TimeSpan.FromMinutes(1)); + var bitmap = FFMpegImage.Snapshot(inputPath, new Size(200, 400), TimeSpan.FromMinutes(1)); // or persists the image on the drive FFMpeg.Snapshot(inputPath, outputPath, new Size(200, 400), TimeSpan.FromMinutes(1)); @@ -61,11 +58,7 @@ await FFMpegArguments } { - FFMpeg.JoinImageSequence(@"..\joined_video.mp4", frameRate: 1, - ImageInfo.FromPath(@"..\1.png"), - ImageInfo.FromPath(@"..\2.png"), - ImageInfo.FromPath(@"..\3.png") - ); + FFMpeg.JoinImageSequence(@"..\joined_video.mp4", frameRate: 1, @"..\1.png", @"..\2.png", @"..\3.png"); } { @@ -84,16 +77,18 @@ await FFMpegArguments var inputImagePath = "/path/to/input/image"; { FFMpeg.PosterWithAudio(inputPath, inputAudioPath, outputPath); - // or - var image = Image.FromFile(inputImagePath); + // or +#pragma warning disable CA1416 + using var image = Image.FromFile(inputImagePath); image.AddAudio(inputAudioPath, outputPath); +#pragma warning restore CA1416 } IVideoFrame GetNextFrame() => throw new NotImplementedException(); { IEnumerable CreateFrames(int count) { - for(int i = 0; i < count; i++) + for (var i = 0; i < count; i++) { yield return GetNextFrame(); //method of generating new frames } @@ -131,4 +126,4 @@ await FFMpegArguments .Configure(options => options.WorkingDirectory = "./CurrentRunWorkingDir") .Configure(options => options.TemporaryFilesFolder = "./CurrentRunTmpFolder") .ProcessAsynchronously(); -} \ No newline at end of file +} diff --git a/FFMpegCore/Extend/BitmapExtensions.cs b/FFMpegCore.Extensions.System.Drawing.Common/BitmapExtensions.cs similarity index 67% rename from FFMpegCore/Extend/BitmapExtensions.cs rename to FFMpegCore.Extensions.System.Drawing.Common/BitmapExtensions.cs index e2f5505..14cecaa 100644 --- a/FFMpegCore/Extend/BitmapExtensions.cs +++ b/FFMpegCore.Extensions.System.Drawing.Common/BitmapExtensions.cs @@ -1,8 +1,6 @@ -using System; -using System.Drawing; -using System.IO; +using System.Drawing; -namespace FFMpegCore.Extend +namespace FFMpegCore.Extensions.System.Drawing.Common { public static class BitmapExtensions { @@ -16,8 +14,11 @@ public static bool AddAudio(this Image poster, string audio, string output) } finally { - if (File.Exists(destination)) File.Delete(destination); + if (File.Exists(destination)) + { + File.Delete(destination); + } } } } -} \ No newline at end of file +} diff --git a/FFMpegCore/Extend/BitmapVideoFrameWrapper.cs b/FFMpegCore.Extensions.System.Drawing.Common/BitmapVideoFrameWrapper.cs similarity index 95% rename from FFMpegCore/Extend/BitmapVideoFrameWrapper.cs rename to FFMpegCore.Extensions.System.Drawing.Common/BitmapVideoFrameWrapper.cs index 2222db6..5462ca2 100644 --- a/FFMpegCore/Extend/BitmapVideoFrameWrapper.cs +++ b/FFMpegCore.Extensions.System.Drawing.Common/BitmapVideoFrameWrapper.cs @@ -1,13 +1,9 @@ -using System; -using System.Drawing; +using System.Drawing; using System.Drawing.Imaging; -using System.IO; using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; using FFMpegCore.Pipes; -namespace FFMpegCore.Extend +namespace FFMpegCore.Extensions.System.Drawing.Common { public class BitmapVideoFrameWrapper : IVideoFrame, IDisposable { 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 new file mode 100644 index 0000000..aafb577 --- /dev/null +++ b/FFMpegCore.Extensions.System.Drawing.Common/FFMpegCore.Extensions.System.Drawing.Common.csproj @@ -0,0 +1,21 @@ + + + + true + Image extension for FFMpegCore using System.Common.Drawing + 5.0.0 + + + ffmpeg ffprobe convert video audio mediafile resize analyze muxing + Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev + + + + + + + + + + + diff --git a/FFMpegCore.Extensions.System.Drawing.Common/FFMpegImage.cs b/FFMpegCore.Extensions.System.Drawing.Common/FFMpegImage.cs new file mode 100644 index 0000000..f36f83d --- /dev/null +++ b/FFMpegCore.Extensions.System.Drawing.Common/FFMpegImage.cs @@ -0,0 +1,56 @@ +using System.Drawing; +using FFMpegCore.Pipes; + +namespace FFMpegCore.Extensions.System.Drawing.Common +{ + 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) + { + 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(); + + 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(); + + await arguments + .OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options + .ForceFormat("rawvideo"))) + .ProcessAsynchronously(); + + ms.Position = 0; + return new Bitmap(ms); + } + } +} diff --git a/FFMpegCore.Test/ArgumentBuilderTest.cs b/FFMpegCore.Test/ArgumentBuilderTest.cs index 9cf7e39..f676a44 100644 --- a/FFMpegCore.Test/ArgumentBuilderTest.cs +++ b/FFMpegCore.Test/ArgumentBuilderTest.cs @@ -1,7 +1,6 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; -using FFMpegCore.Arguments; +using FFMpegCore.Arguments; using FFMpegCore.Enums; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace FFMpegCore.Test { @@ -10,7 +9,6 @@ public class ArgumentBuilderTest { private readonly string[] _concatFiles = { "1.mp4", "2.mp4", "3.mp4", "4.mp4" }; - [TestMethod] public void Builder_BuildString_IO_1() { @@ -53,7 +51,6 @@ public void Builder_BuildString_Quiet() Assert.AreEqual("-hide_banner -loglevel error -i \"input.mp4\" \"output.mp4\"", str); } - [TestMethod] public void Builder_BuildString_AudioCodec_Fluent() { @@ -75,7 +72,7 @@ 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 \"output.mp4\"", str); + Assert.AreEqual("-i \"input.mp4\" -hwaccel auto \"output.mp4\"", str); } [TestMethod] @@ -114,7 +111,7 @@ 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 copy \"output.mp4\"", str); + Assert.AreEqual("-i \"input.mp4\" -c:a copy -c:v copy \"output.mp4\"", str); } [TestMethod] @@ -400,7 +397,6 @@ public void Builder_BuildString_Codec_Override() Assert.AreEqual("-i \"input.mp4\" -c:v libx264 -pix_fmt yuv420p \"output.mp4\" -y", str); } - [TestMethod] public void Builder_BuildString_Duration() { @@ -421,7 +417,6 @@ public void Builder_BuildString_Raw() Assert.AreEqual("-i \"input.mp4\" -acodec copy \"output.mp4\"", str); } - [TestMethod] public void Builder_BuildString_ForcePixelFormat() { @@ -495,5 +490,45 @@ public void Builder_BuildString_Audible_AAXC_Decryption() 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); + } } -} \ No newline at end of file +} diff --git a/FFMpegCore.Test/AudioTest.cs b/FFMpegCore.Test/AudioTest.cs index 795fedf..ba4e3eb 100644 --- a/FFMpegCore.Test/AudioTest.cs +++ b/FFMpegCore.Test/AudioTest.cs @@ -4,25 +4,20 @@ using FFMpegCore.Pipes; using FFMpegCore.Test.Resources; using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; namespace FFMpegCore.Test { [TestClass] - public class AudioTest + public class AudioTest { [TestMethod] public void Audio_Remove() { using var outputFile = new TemporaryFile("out.mp4"); - + FFMpeg.Mute(TestResources.Mp4Video, outputFile); var analysis = FFProbe.Analyse(outputFile); - + Assert.IsTrue(analysis.VideoStreams.Any()); Assert.IsTrue(!analysis.AudioStreams.Any()); } @@ -31,10 +26,10 @@ public void Audio_Remove() public void Audio_Save() { using var outputFile = new TemporaryFile("out.mp3"); - + FFMpeg.ExtractAudio(TestResources.Mp4Video, outputFile); var analysis = FFProbe.Analyse(outputFile); - + Assert.IsTrue(!analysis.VideoStreams.Any()); Assert.IsTrue(analysis.AudioStreams.Any()); } @@ -48,17 +43,17 @@ await FFMpegArguments .OutputToPipe(new StreamPipeSink(memoryStream), options => options.ForceFormat("mp3")) .ProcessAsynchronously(); } - + [TestMethod] public void Audio_Add() { using var outputFile = new TemporaryFile("out.mp4"); - + var success = FFMpeg.ReplaceAudio(TestResources.Mp4WithoutAudio, TestResources.Mp3Audio, outputFile); var videoAnalysis = FFProbe.Analyse(TestResources.Mp4WithoutAudio); var audioAnalysis = FFProbe.Analyse(TestResources.Mp3Audio); var outputAnalysis = FFProbe.Analyse(outputFile); - + Assert.IsTrue(success); Assert.AreEqual(Math.Max(videoAnalysis.Duration.TotalSeconds, audioAnalysis.Duration.TotalSeconds), outputAnalysis.Duration.TotalSeconds, 0.15); Assert.IsTrue(File.Exists(outputFile)); @@ -239,7 +234,7 @@ public void Audio_Pan_ToMono() Assert.IsTrue(success); Assert.AreEqual(1, mediaAnalysis.AudioStreams.Count); - Assert.AreEqual("mono", mediaAnalysis.PrimaryAudioStream.ChannelLayout); + Assert.AreEqual("mono", mediaAnalysis.PrimaryAudioStream!.ChannelLayout); } [TestMethod, Timeout(10000)] @@ -257,7 +252,7 @@ public void Audio_Pan_ToMonoNoDefinitions() Assert.IsTrue(success); Assert.AreEqual(1, mediaAnalysis.AudioStreams.Count); - Assert.AreEqual("mono", mediaAnalysis.PrimaryAudioStream.ChannelLayout); + Assert.AreEqual("mono", mediaAnalysis.PrimaryAudioStream!.ChannelLayout); } [TestMethod, Timeout(10000)] @@ -330,4 +325,4 @@ public void Audio_DynamicNormalizer_FilterWindow(int filterWindow) .ProcessSynchronously()); } } -} \ No newline at end of file +} diff --git a/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs b/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs index 6e30999..bb7071d 100644 --- a/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs +++ b/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs @@ -1,7 +1,7 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using FluentAssertions; -using System.Reflection; +using System.Reflection; using FFMpegCore.Arguments; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace FFMpegCore.Test { @@ -13,14 +13,13 @@ public void TestInitialize() { // 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); + 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() { @@ -44,14 +43,12 @@ public void Processor_SessionOptions_GetUsed() options.WorkingDirectory.Should().Be(sessionWorkingDir); } - [TestMethod] public void Processor_Options_CanBeOverridden_And_Configured() { var globalConfig = "Whatever"; GlobalFFOptions.Configure(new FFOptions { WorkingDirectory = globalConfig, TemporaryFilesFolder = globalConfig, BinaryFolder = globalConfig }); - var processor = CreateArgumentProcessor(); var sessionTempDir = "./CurrentRunWorkingDir"; @@ -65,14 +62,9 @@ public void Processor_Options_CanBeOverridden_And_Configured() options.BinaryFolder.Should().NotBeEquivalentTo(globalConfig); } - [TestMethod] public void Options_Global_And_Session_Options_Can_Differ() { - FFMpegArgumentProcessor CreateArgumentProcessor() => FFMpegArguments - .FromFileInput("") - .OutputToFile(""); - var globalWorkingDir = "Whatever"; GlobalFFOptions.Configure(new FFOptions { WorkingDirectory = globalWorkingDir }); @@ -82,7 +74,6 @@ FFMpegArgumentProcessor CreateArgumentProcessor() => FFMpegArguments var options1 = processor1.GetConfiguredOptions(null); options1.WorkingDirectory.Should().Be(sessionWorkingDir); - var processor2 = CreateArgumentProcessor(); var options2 = processor2.GetConfiguredOptions(null); options2.WorkingDirectory.Should().Be(globalWorkingDir); @@ -102,7 +93,6 @@ public void Audible_Aaxc_Test() arg.Text.Should().Be($"-audible_key 123 -audible_iv 456"); } - [TestMethod] public void Audible_Aax_Test() { @@ -110,4 +100,4 @@ public void Audible_Aax_Test() arg.Text.Should().Be($"-activation_bytes 62689101"); } } -} \ No newline at end of file +} diff --git a/FFMpegCore.Test/FFMpegCore.Test.csproj b/FFMpegCore.Test/FFMpegCore.Test.csproj index d281c3d..def07d2 100644 --- a/FFMpegCore.Test/FFMpegCore.Test.csproj +++ b/FFMpegCore.Test/FFMpegCore.Test.csproj @@ -2,16 +2,41 @@ net6.0 - false - disable - default - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest @@ -21,6 +46,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -28,69 +56,53 @@ PreserveNewest - Always - - - - - PreserveNewest - - - - - - - - - - - - - - - - + - Always + PreserveNewest - Always + PreserveNewest - Always + PreserveNewest - Always + PreserveNewest - Always + PreserveNewest - Always + PreserveNewest - Always + PreserveNewest - Always + PreserveNewest - Always + PreserveNewest - Always + PreserveNewest - Always + PreserveNewest PreserveNewest - - - - + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + diff --git a/FFMpegCore.Test/FFMpegOptionsTests.cs b/FFMpegCore.Test/FFMpegOptionsTests.cs index 2be810f..7cab476 100644 --- a/FFMpegCore.Test/FFMpegOptionsTests.cs +++ b/FFMpegCore.Test/FFMpegOptionsTests.cs @@ -1,6 +1,5 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json; -using System.IO; namespace FFMpegCore.Test { @@ -23,7 +22,7 @@ public void Options_Defaults_Configured() public void Options_Loaded_From_File() { Assert.AreEqual( - GlobalFFOptions.Current.BinaryFolder, + GlobalFFOptions.Current.BinaryFolder, JsonConvert.DeserializeObject(File.ReadAllText("ffmpeg.config.json")).BinaryFolder ); } @@ -31,7 +30,7 @@ public void Options_Loaded_From_File() [TestMethod] public void Options_Set_Programmatically() { - var original = GlobalFFOptions.Current; + var original = GlobalFFOptions.Current; try { GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "Whatever" }); diff --git a/FFMpegCore.Test/FFProbeTests.cs b/FFMpegCore.Test/FFProbeTests.cs index a4d836d..9da819a 100644 --- a/FFMpegCore.Test/FFProbeTests.cs +++ b/FFMpegCore.Test/FFProbeTests.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using FFMpegCore.Test.Resources; +using FFMpegCore.Test.Resources; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace FFMpegCore.Test @@ -24,7 +19,7 @@ public async Task Audio_FromStream_Duration() 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)); @@ -36,7 +31,7 @@ public void FrameAnalysis_Sync() public async Task FrameAnalysis_Async() { var frameAnalysis = await FFProbe.GetFramesAsync(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)); @@ -55,12 +50,11 @@ public async Task PacketAnalysis_Async() Assert.AreEqual(1362, packets.Last().Size); } - [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.AreEqual("K_", packets[0].Flags); @@ -74,9 +68,9 @@ public void PacketAnalysisAudioVideo_Sync() Assert.AreEqual(216, packets.Count); var actual = packets.Select(f => f.CodecType).Distinct().ToList(); - var expected = new List {"audio", "video"}; + var expected = new List { "audio", "video" }; CollectionAssert.AreEquivalent(expected, actual); - Assert.IsTrue(packets.Where(t=>t.CodecType == "audio").All(f => f.Flags == "K_")); + Assert.IsTrue(packets.Where(t => t.CodecType == "audio").All(f => f.Flags == "K_")); Assert.AreEqual(75, packets.Count(t => t.CodecType == "video")); Assert.AreEqual(141, packets.Count(t => t.CodecType == "audio")); } @@ -90,7 +84,7 @@ public void MediaAnalysis_ParseDuration(string duration, int expectedDays, int e { var ffprobeStream = new FFProbeStream { Duration = duration }; - var parsedDuration = MediaAnalysisUtils.ParseDuration(ffprobeStream); + var parsedDuration = MediaAnalysisUtils.ParseDuration(ffprobeStream.Duration); Assert.AreEqual(expectedDays, parsedDuration.Days); Assert.AreEqual(expectedHours, parsedDuration.Hours); @@ -99,19 +93,19 @@ public void MediaAnalysis_ParseDuration(string duration, int expectedDays, int e Assert.AreEqual(expectedMilliseconds, parsedDuration.Milliseconds); } - [TestMethod] + [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] public void Probe_Success() { var info = FFProbe.Analyse(TestResources.Mp4Video); Assert.AreEqual(3, info.Duration.Seconds); - + Assert.AreEqual("5.1", info.PrimaryAudioStream!.ChannelLayout); Assert.AreEqual(6, info.PrimaryAudioStream.Channels); Assert.AreEqual("AAC (Advanced Audio Coding)", info.PrimaryAudioStream.CodecLongName); @@ -121,10 +115,12 @@ public void Probe_Success() 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(1280, info.PrimaryVideoStream.Width); Assert.AreEqual(720, info.PrimaryVideoStream.Height); @@ -137,12 +133,25 @@ public void Probe_Success() 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); + + info = FFProbe.Analyse(TestResources.Mp4VideoRotation); + 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)] @@ -151,6 +160,8 @@ 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)] @@ -169,6 +180,8 @@ public async Task Probe_Success_Subtitle_Async() 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)] @@ -180,5 +193,47 @@ public async Task Probe_Success_Disposition_Async() Assert.AreEqual(true, info.PrimaryAudioStream.Disposition["default"]); Assert.AreEqual(false, 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)] + 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)] + 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)] + public async Task Probe_Success_24BitWavBitDepth_Async() + { + var info = await FFProbe.AnalyseAsync(TestResources.Wav24Bit); + Assert.IsNotNull(info.PrimaryAudioStream); + Assert.AreEqual(24, 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); + } } -} \ No newline at end of file +} diff --git a/FFMpegCore.Test/MetaDataBuilderTests.cs b/FFMpegCore.Test/MetaDataBuilderTests.cs index 747fd9e..3a2b81a 100644 --- a/FFMpegCore.Test/MetaDataBuilderTests.cs +++ b/FFMpegCore.Test/MetaDataBuilderTests.cs @@ -1,15 +1,7 @@ -using FFMpegCore.Builders.MetaData; - +using System.Text.RegularExpressions; +using FFMpegCore.Builders.MetaData; using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; - namespace FFMpegCore.Test { [TestClass] @@ -71,8 +63,6 @@ public void TestMapMetadata() .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."); } diff --git a/FFMpegCore.Test/PixelFormatTests.cs b/FFMpegCore.Test/PixelFormatTests.cs index 2c22fc5..ed69f11 100644 --- a/FFMpegCore.Test/PixelFormatTests.cs +++ b/FFMpegCore.Test/PixelFormatTests.cs @@ -24,7 +24,7 @@ public void PixelFormats_TryGetNotExisting() { Assert.IsFalse(FFMpeg.TryGetPixelFormat("yuv420pppUnknown", out _)); } - + [TestMethod] public void PixelFormats_GetExisting() { diff --git a/FFMpegCore.Test/Resources/24_bit_fixed.WAV b/FFMpegCore.Test/Resources/24_bit_fixed.WAV new file mode 100644 index 0000000..acd1265 Binary files /dev/null and b/FFMpegCore.Test/Resources/24_bit_fixed.WAV differ diff --git a/FFMpegCore.Test/Resources/32_bit_float.WAV b/FFMpegCore.Test/Resources/32_bit_float.WAV new file mode 100644 index 0000000..46bfa29 Binary files /dev/null and b/FFMpegCore.Test/Resources/32_bit_float.WAV differ diff --git a/FFMpegCore.Test/Resources/TestResources.cs b/FFMpegCore.Test/Resources/TestResources.cs index 14f8abe..de84080 100644 --- a/FFMpegCore.Test/Resources/TestResources.cs +++ b/FFMpegCore.Test/Resources/TestResources.cs @@ -13,6 +13,7 @@ public enum ImageType 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 WebmVideo = "./Resources/input_3sec.webm"; public static readonly string Mp4WithoutVideo = "./Resources/input_audio_only_10sec.mp4"; public static readonly string Mp4WithoutAudio = "./Resources/input_video_only_3sec.mp4"; @@ -21,5 +22,9 @@ public static class TestResources 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/Resources/input_3sec_rotation_90deg.mp4 b/FFMpegCore.Test/Resources/input_3sec_rotation_90deg.mp4 new file mode 100644 index 0000000..c2a9ee2 Binary files /dev/null and b/FFMpegCore.Test/Resources/input_3sec_rotation_90deg.mp4 differ diff --git a/FFMpegCore.Test/Resources/sample3aiff.aiff b/FFMpegCore.Test/Resources/sample3aiff.aiff new file mode 100644 index 0000000..b9973ae Binary files /dev/null and b/FFMpegCore.Test/Resources/sample3aiff.aiff differ diff --git a/FFMpegCore.Test/Resources/sampleMKV.mkv b/FFMpegCore.Test/Resources/sampleMKV.mkv new file mode 100644 index 0000000..5b162de Binary files /dev/null and b/FFMpegCore.Test/Resources/sampleMKV.mkv differ diff --git a/FFMpegCore.Test/TemporaryFile.cs b/FFMpegCore.Test/TemporaryFile.cs index f64f5fe..dc30ca4 100644 --- a/FFMpegCore.Test/TemporaryFile.cs +++ b/FFMpegCore.Test/TemporaryFile.cs @@ -1,7 +1,4 @@ -using System; -using System.IO; - -namespace FFMpegCore.Test +namespace FFMpegCore.Test { public class TemporaryFile : IDisposable { @@ -16,7 +13,9 @@ public TemporaryFile(string filename) public void Dispose() { if (File.Exists(_path)) + { File.Delete(_path); + } } } -} \ No newline at end of file +} diff --git a/FFMpegCore.Test/Utilities/BitmapSources.cs b/FFMpegCore.Test/Utilities/BitmapSources.cs index 8ea02e8..b7ecb45 100644 --- a/FFMpegCore.Test/Utilities/BitmapSources.cs +++ b/FFMpegCore.Test/Utilities/BitmapSources.cs @@ -1,18 +1,18 @@ -using FFMpegCore.Extend; -using System; -using System.Collections.Generic; -using System.Drawing; +using System.Drawing; using System.Drawing.Imaging; using System.Numerics; +using System.Runtime.Versioning; +using FFMpegCore.Extensions.System.Drawing.Common; using FFMpegCore.Pipes; -namespace FFMpegCore.Test +namespace FFMpegCore.Test.Utilities { - static class BitmapSource + [SupportedOSPlatform("windows")] + internal static class BitmapSource { public static IEnumerable CreateBitmaps(int count, PixelFormat fmt, int w, int h) { - for (int 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)) { @@ -27,8 +27,9 @@ public static BitmapVideoFrameWrapper CreateVideoFrame(int index, PixelFormat fm offset = offset * index; - for (int y = 0; y < h; y++) - for (int x = 0; x < w; x++) + for (var y = 0; y < h; y++) + { + for (var x = 0; x < w; x++) { var xf = x / (float)w; var yf = y / (float)h; @@ -41,6 +42,7 @@ public static BitmapVideoFrameWrapper CreateVideoFrame(int index, PixelFormat fm bitmap.SetPixel(x, y, color); } + } return new BitmapVideoFrameWrapper(bitmap); } @@ -53,7 +55,7 @@ public static BitmapVideoFrameWrapper CreateVideoFrame(int index, PixelFormat fm // Based on the original implementation by Ken Perlin // http://mrl.nyu.edu/~perlin/noise/ // - static class Perlin + private static class Perlin { #region Noise functions @@ -126,6 +128,7 @@ public static float Fbm(float x, int octave) x *= 2.0f; w *= 0.5f; } + return f; } @@ -139,6 +142,7 @@ public static float Fbm(Vector2 coord, int octave) coord *= 2.0f; w *= 0.5f; } + return f; } @@ -157,6 +161,7 @@ public static float Fbm(Vector3 coord, int octave) coord *= 2.0f; w *= 0.5f; } + return f; } @@ -169,27 +174,27 @@ public static float Fbm(float x, float y, float z, int octave) #region Private functions - static float Fade(float t) + private static float Fade(float t) { return t * t * t * (t * (t * 6 - 15) + 10); } - static float Lerp(float t, float a, float b) + private static float Lerp(float t, float a, float b) { return a + t * (b - a); } - static float Grad(int hash, float x) + private static float Grad(int hash, float x) { return (hash & 1) == 0 ? x : -x; } - static float Grad(int hash, float x, float y) + private static float Grad(int hash, float x, float y) { return ((hash & 1) == 0 ? x : -x) + ((hash & 2) == 0 ? y : -y); } - static float Grad(int hash, float x, float y, float z) + private static float Grad(int hash, float x, float y, float z) { var h = hash & 15; var u = h < 8 ? x : y; @@ -197,7 +202,7 @@ static float Grad(int hash, float x, float y, float z) return ((h & 1) == 0 ? u : -u) + ((h & 2) == 0 ? v : -v); } - static int[] perm = { + 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, diff --git a/FFMpegCore.Test/Utilities/WindowsOnlyDataTestMethod.cs b/FFMpegCore.Test/Utilities/WindowsOnlyDataTestMethod.cs new file mode 100644 index 0000000..84a779a --- /dev/null +++ b/FFMpegCore.Test/Utilities/WindowsOnlyDataTestMethod.cs @@ -0,0 +1,23 @@ +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 new file mode 100644 index 0000000..7e817bf --- /dev/null +++ b/FFMpegCore.Test/Utilities/WindowsOnlyTestMethod.cs @@ -0,0 +1,23 @@ +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 262bd75..e3e4b6b 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -1,18 +1,14 @@ -using FFMpegCore.Enums; -using FFMpegCore.Test.Resources; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Imaging; -using System.IO; -using System.Linq; +using System.Drawing.Imaging; +using System.Runtime.Versioning; using System.Text; -using System.Threading.Tasks; using FFMpegCore.Arguments; +using FFMpegCore.Enums; using FFMpegCore.Exceptions; +using FFMpegCore.Extensions.System.Drawing.Common; using FFMpegCore.Pipes; -using System.Threading; +using FFMpegCore.Test.Resources; +using FFMpegCore.Test.Utilities; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace FFMpegCore.Test { @@ -23,7 +19,7 @@ public class VideoTest public void Video_ToOGV() { using var outputFile = new TemporaryFile($"out{VideoType.Ogv.Extension}"); - + var success = FFMpegArguments .FromFileInput(TestResources.WebmVideo) .OutputToFile(outputFile, false) @@ -35,7 +31,7 @@ public void Video_ToOGV() public void Video_ToMP4() { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); - + var success = FFMpegArguments .FromFileInput(TestResources.WebmVideo) .OutputToFile(outputFile, false) @@ -47,7 +43,7 @@ public void Video_ToMP4() 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 @@ -63,7 +59,7 @@ public void Video_ToMP4_YUV444p() 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 @@ -76,7 +72,7 @@ public void Video_ToMP4_Args() public void Video_ToH265_MKV_Args() { using var outputFile = new TemporaryFile($"out.mkv"); - + var success = FFMpegArguments .FromFileInput(TestResources.WebmVideo) .OutputToFile(outputFile, false, opt => opt @@ -85,7 +81,8 @@ public void Video_ToH265_MKV_Args() Assert.IsTrue(success); } - [DataTestMethod, Timeout(10000)] + [SupportedOSPlatform("windows")] + [WindowsOnlyDataTestMethod, Timeout(10000)] [DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)] [DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)] public void Video_ToMP4_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat) @@ -101,12 +98,13 @@ public void Video_ToMP4_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat Assert.IsTrue(success); } - [TestMethod, Timeout(10000)] + [SupportedOSPlatform("windows")] + [WindowsOnlyTestMethod, Timeout(10000)] public void Video_ToMP4_Args_Pipe_DifferentImageSizes() { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); - var frames = new List + var frames = new List { BitmapSource.CreateVideoFrame(0, System.Drawing.Imaging.PixelFormat.Format24bppRgb, 255, 255, 1, 0), BitmapSource.CreateVideoFrame(0, System.Drawing.Imaging.PixelFormat.Format24bppRgb, 256, 256, 1, 0) @@ -120,8 +118,8 @@ public void Video_ToMP4_Args_Pipe_DifferentImageSizes() .ProcessSynchronously()); } - - [TestMethod, Timeout(10000)] + [SupportedOSPlatform("windows")] + [WindowsOnlyTestMethod, Timeout(10000)] public async Task Video_ToMP4_Args_Pipe_DifferentImageSizes_Async() { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); @@ -140,7 +138,8 @@ public async Task Video_ToMP4_Args_Pipe_DifferentImageSizes_Async() .ProcessAsynchronously()); } - [TestMethod, Timeout(10000)] + [SupportedOSPlatform("windows")] + [WindowsOnlyTestMethod, Timeout(10000)] public void Video_ToMP4_Args_Pipe_DifferentPixelFormats() { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); @@ -159,8 +158,8 @@ public void Video_ToMP4_Args_Pipe_DifferentPixelFormats() .ProcessSynchronously()); } - - [TestMethod, Timeout(10000)] + [SupportedOSPlatform("windows")] + [WindowsOnlyTestMethod, Timeout(10000)] public async Task Video_ToMP4_Args_Pipe_DifferentPixelFormats_Async() { using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}"); @@ -184,7 +183,7 @@ 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 @@ -207,7 +206,6 @@ await FFMpegArguments }); } - [TestMethod, Timeout(10000)] public void Video_StreamFile_OutputToMemoryStream() { @@ -239,7 +237,6 @@ public void Video_ToMP4_Args_StreamOutputPipe_Failure() }); } - [TestMethod, Timeout(10000)] public async Task Video_ToMP4_Args_StreamOutputPipe_Async() { @@ -260,12 +257,12 @@ public async Task TestDuplicateRun() .FromFileInput(TestResources.Mp4Video) .OutputToFile("temporary.mp4") .ProcessSynchronously(); - + await FFMpegArguments .FromFileInput(TestResources.Mp4Video) .OutputToFile("temporary.mp4") .ProcessAsynchronously(); - + File.Delete("temporary.mp4"); } @@ -291,7 +288,7 @@ public void TranscodeToMemoryStream_Success() public void Video_ToTS() { using var outputFile = new TemporaryFile($"out{VideoType.MpegTs.Extension}"); - + var success = FFMpegArguments .FromFileInput(TestResources.Mp4Video) .OutputToFile(outputFile, false) @@ -303,7 +300,7 @@ public void Video_ToTS() 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 @@ -314,14 +311,15 @@ public void Video_ToTS_Args() Assert.IsTrue(success); } - [DataTestMethod, Timeout(10000)] + [SupportedOSPlatform("windows")] + [WindowsOnlyDataTestMethod, Timeout(10000)] [DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)] [DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)] public async Task Video_ToTS_Args_Pipe(System.Drawing.Imaging.PixelFormat 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 @@ -346,15 +344,16 @@ public async Task Video_ToOGV_Resize() Assert.IsTrue(success); } - [DataTestMethod, Timeout(10000)] + [SupportedOSPlatform("windows")] + [WindowsOnlyDataTestMethod, Timeout(10000)] [DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)] [DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)] - // [DataRow(PixelFormat.Format48bppRgb)] + [DataRow(System.Drawing.Imaging.PixelFormat.Format48bppRgb)] public void RawVideoPipeSource_Ogv_Scale(System.Drawing.Imaging.PixelFormat 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 @@ -371,7 +370,7 @@ public void RawVideoPipeSource_Ogv_Scale(System.Drawing.Imaging.PixelFormat pixe 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 @@ -381,7 +380,8 @@ public void Scale_Mp4_Multithreaded() Assert.IsTrue(success); } - [DataTestMethod, Timeout(10000)] + [SupportedOSPlatform("windows")] + [WindowsOnlyDataTestMethod, Timeout(10000)] [DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)] [DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)] // [DataRow(PixelFormat.Format48bppRgb)] @@ -389,7 +389,7 @@ public void Video_ToMP4_Resize_Args_Pipe(System.Drawing.Imaging.PixelFormat pixe { 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 @@ -398,12 +398,13 @@ public void Video_ToMP4_Resize_Args_Pipe(System.Drawing.Imaging.PixelFormat pixe Assert.IsTrue(success); } - [TestMethod, Timeout(10000)] + [SupportedOSPlatform("windows")] + [WindowsOnlyTestMethod, Timeout(10000)] public void Video_Snapshot_InMemory() { + using var bitmap = FFMpegImage.Snapshot(TestResources.Mp4Video); + var input = FFProbe.Analyse(TestResources.Mp4Video); - using var bitmap = FFMpeg.Snapshot(TestResources.Mp4Video); - Assert.AreEqual(input.PrimaryVideoStream!.Width, bitmap.Width); Assert.AreEqual(input.PrimaryVideoStream.Height, bitmap.Height); Assert.AreEqual(bitmap.RawFormat, ImageFormat.Png); @@ -417,10 +418,10 @@ public void Video_Snapshot_PersistSnapshot() FFMpeg.Snapshot(TestResources.Mp4Video, outputPath); - using var bitmap = Image.FromFile(outputPath); - Assert.AreEqual(input.PrimaryVideoStream!.Width, bitmap.Width); - Assert.AreEqual(input.PrimaryVideoStream.Height, bitmap.Height); - Assert.AreEqual(bitmap.RawFormat, ImageFormat.Png); + 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(10000)] @@ -428,13 +429,13 @@ public void Video_Join() { var inputCopy = new TemporaryFile("copy-input.mp4"); File.Copy(TestResources.Mp4Video, inputCopy); - + var outputPath = new TemporaryFile("out.mp4"); var input = FFProbe.Analyse(TestResources.Mp4Video); var success = FFMpeg.Join(outputPath, TestResources.Mp4Video, inputCopy); Assert.IsTrue(success); Assert.IsTrue(File.Exists(outputPath)); - + var expectedDuration = input.Duration * 2; var result = FFProbe.Analyse(outputPath); Assert.AreEqual(expectedDuration.Days, result.Duration.Days); @@ -445,28 +446,29 @@ public void Video_Join() Assert.AreEqual(input.PrimaryVideoStream.Width, result.PrimaryVideoStream.Width); } - [TestMethod, Timeout(10000)] + [TestMethod, Timeout(20000)] public void Video_Join_Image_Sequence() { - var imageSet = new List(); - Directory.EnumerateFiles(TestResources.ImageCollection) - .Where(file => file.ToLower().EndsWith(".png")) + var imageSet = new List(); + Directory.EnumerateFiles(TestResources.ImageCollection, "*.png") .ToList() .ForEach(file => { - for (var i = 0; i < 15; i++) + for (var i = 0; i < 5; i++) { - imageSet.Add(new ImageInfo(file)); + imageSet.Add(file); } }); + var imageAnalysis = FFProbe.Analyse(imageSet.First()); - using var outputFile = new TemporaryFile("out.mp4"); - var success = FFMpeg.JoinImageSequence(outputFile, images: imageSet.ToArray()); + 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(3, result.Duration.Seconds); - Assert.AreEqual(imageSet.First().Width, result.PrimaryVideoStream!.Width); - Assert.AreEqual(imageSet.First().Height, result.PrimaryVideoStream.Height); + + Assert.AreEqual(1, result.Duration.Seconds); + Assert.AreEqual(imageAnalysis.PrimaryVideoStream!.Width, result.PrimaryVideoStream!.Width); + Assert.AreEqual(imageAnalysis.PrimaryVideoStream!.Height, result.PrimaryVideoStream.Height); } [TestMethod, Timeout(10000)] @@ -505,14 +507,28 @@ public void Video_UpdatesProgress() var percentageDone = 0.0; var timeDone = TimeSpan.Zero; - void OnPercentageProgess(double percentage) => percentageDone = percentage; - void OnTimeProgess(TimeSpan time) => timeDone = time; - 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(TimeSpan.FromSeconds(2))) + .WithDuration(analysis.Duration)) .NotifyOnProgress(OnPercentageProgess, analysis.Duration) .NotifyOnProgress(OnTimeProgess) .ProcessSynchronously(); @@ -520,7 +536,9 @@ public void Video_UpdatesProgress() 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(10000)] @@ -528,7 +546,7 @@ public void Video_OutputsData() { var outputFile = new TemporaryFile("out.mp4"); var dataReceived = false; - + GlobalFFOptions.Configure(opt => opt.Encoding = Encoding.UTF8); var success = FFMpegArguments .FromFileInput(TestResources.Mp4Video) @@ -544,7 +562,8 @@ public void Video_OutputsData() Assert.IsTrue(File.Exists(outputFile)); } - [TestMethod, Timeout(10000)] + [SupportedOSPlatform("windows")] + [WindowsOnlyTestMethod, Timeout(10000)] public void Video_TranscodeInMemory() { using var resStream = new MemoryStream(); @@ -564,6 +583,24 @@ public void Video_TranscodeInMemory() Assert.AreEqual(vi.PrimaryVideoStream.Height, 128); } + [TestMethod, Timeout(20000)] + 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(10000)] public async Task Video_Cancel_Async() { diff --git a/FFMpegCore.sln b/FFMpegCore.sln index 7a27980..5a9faa8 100644 --- a/FFMpegCore.sln +++ b/FFMpegCore.sln @@ -9,6 +9,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMpegCore.Test", "FFMpegCo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMpegCore.Examples", "FFMpegCore.Examples\FFMpegCore.Examples.csproj", "{3125CF91-FFBD-4E4E-8930-247116AFE772}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FFMpegCore.Extensions.System.Drawing.Common", "FFMpegCore.Extensions.System.Drawing.Common\FFMpegCore.Extensions.System.Drawing.Common.csproj", "{9C1A4930-9369-4A18-AD98-929A2A510D80}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +29,10 @@ Global {3125CF91-FFBD-4E4E-8930-247116AFE772}.Debug|Any CPU.Build.0 = Debug|Any CPU {3125CF91-FFBD-4E4E-8930-247116AFE772}.Release|Any CPU.ActiveCfg = Release|Any CPU {3125CF91-FFBD-4E4E-8930-247116AFE772}.Release|Any CPU.Build.0 = Release|Any CPU + {9C1A4930-9369-4A18-AD98-929A2A510D80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9C1A4930-9369-4A18-AD98-929A2A510D80}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9C1A4930-9369-4A18-AD98-929A2A510D80}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9C1A4930-9369-4A18-AD98-929A2A510D80}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/FFMpegCore/Assembly.cs b/FFMpegCore/Assembly.cs index 0117671..3ee43ab 100644 --- a/FFMpegCore/Assembly.cs +++ b/FFMpegCore/Assembly.cs @@ -1,3 +1,3 @@ using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("FFMpegCore.Test")] \ No newline at end of file +[assembly: InternalsVisibleTo("FFMpegCore.Test")] diff --git a/FFMpegCore/Extend/KeyValuePairExtensions.cs b/FFMpegCore/Extend/KeyValuePairExtensions.cs index c2c6813..4af0904 100644 --- a/FFMpegCore/Extend/KeyValuePairExtensions.cs +++ b/FFMpegCore/Extend/KeyValuePairExtensions.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace FFMpegCore.Extend +namespace FFMpegCore.Extend { internal static class KeyValuePairExtensions { @@ -21,4 +19,4 @@ public static string FormatArgumentPair(this KeyValuePair pair, return $"{key}={value}"; } } -} \ No newline at end of file +} diff --git a/FFMpegCore/Extend/PcmAudioSampleWrapper.cs b/FFMpegCore/Extend/PcmAudioSampleWrapper.cs index d6c1d2f..7548d63 100644 --- a/FFMpegCore/Extend/PcmAudioSampleWrapper.cs +++ b/FFMpegCore/Extend/PcmAudioSampleWrapper.cs @@ -1,7 +1,4 @@ -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using FFMpegCore.Pipes; +using FFMpegCore.Pipes; namespace FFMpegCore.Extend { diff --git a/FFMpegCore/Extend/StringExtensions.cs b/FFMpegCore/Extend/StringExtensions.cs index 7b02089..96bcd6c 100644 --- a/FFMpegCore/Extend/StringExtensions.cs +++ b/FFMpegCore/Extend/StringExtensions.cs @@ -1,12 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Text; +using System.Text; namespace FFMpegCore.Extend { internal static class StringExtensions { - private static Dictionary CharactersSubstitution { get; } = new Dictionary + private static Dictionary CharactersSubstitution { get; } = new() { { '\\', @"\\" }, { ':', @"\:" }, @@ -68,4 +66,4 @@ public static string Replace(this string str, Dictionary replaceLi return parsedString.ToString(); } } -} \ No newline at end of file +} diff --git a/FFMpegCore/Extend/UriExtensions.cs b/FFMpegCore/Extend/UriExtensions.cs index ebe92c0..9427883 100644 --- a/FFMpegCore/Extend/UriExtensions.cs +++ b/FFMpegCore/Extend/UriExtensions.cs @@ -1,6 +1,4 @@ -using System; - -namespace FFMpegCore.Extend +namespace FFMpegCore.Extend { public static class UriExtensions { @@ -9,4 +7,4 @@ public static bool SaveStream(this Uri uri, string output) return FFMpeg.SaveM3U8Stream(uri, output); } } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Arguments/AudibleEncryptionKeyArgument.cs b/FFMpegCore/FFMpeg/Arguments/AudibleEncryptionKeyArgument.cs index 0f514dc..5f7b7dc 100644 --- a/FFMpegCore/FFMpeg/Arguments/AudibleEncryptionKeyArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/AudibleEncryptionKeyArgument.cs @@ -3,12 +3,11 @@ public class AudibleEncryptionKeyArgument : IArgument { private readonly bool _aaxcMode; - - private readonly string _key; - private readonly string _iv; - private readonly string _activationBytes; + private readonly string? _key; + private readonly string? _iv; + private readonly string? _activationBytes; public AudibleEncryptionKeyArgument(string activationBytes) { diff --git a/FFMpegCore/FFMpeg/Arguments/AudioBitrateArgument.cs b/FFMpegCore/FFMpeg/Arguments/AudioBitrateArgument.cs index 9c7e813..5e3ed9f 100644 --- a/FFMpegCore/FFMpeg/Arguments/AudioBitrateArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/AudioBitrateArgument.cs @@ -14,7 +14,6 @@ public AudioBitrateArgument(int bitrate) Bitrate = bitrate; } - public string Text => $"-b:a {Bitrate}k"; } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Arguments/AudioCodecArgument.cs b/FFMpegCore/FFMpeg/Arguments/AudioCodecArgument.cs index 273bb02..e970008 100644 --- a/FFMpegCore/FFMpeg/Arguments/AudioCodecArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/AudioCodecArgument.cs @@ -13,7 +13,9 @@ public class AudioCodecArgument : IArgument public AudioCodecArgument(Codec audioCodec) { if (audioCodec.Type != CodecType.Audio) + { throw new FFMpegException(FFMpegExceptionType.Operation, $"Codec \"{audioCodec.Name}\" is not an audio codec"); + } AudioCodec = audioCodec.Name; } diff --git a/FFMpegCore/FFMpeg/Arguments/AudioFiltersArgument.cs b/FFMpegCore/FFMpeg/Arguments/AudioFiltersArgument.cs index 50b26b3..bbcecb3 100644 --- a/FFMpegCore/FFMpeg/Arguments/AudioFiltersArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/AudioFiltersArgument.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; -using System.Linq; -using FFMpegCore.Exceptions; +using FFMpegCore.Exceptions; namespace FFMpegCore.Arguments { @@ -18,7 +16,9 @@ public AudioFiltersArgument(AudioFilterOptions options) private string GetText() { if (!Options.Arguments.Any()) + { throw new FFMpegArgumentException("No audio-filter arguments provided"); + } var arguments = Options.Arguments .Where(arg => !string.IsNullOrEmpty(arg.Value)) @@ -34,13 +34,13 @@ private string GetText() public interface IAudioFilterArgument { - public string Key { get; } - public string Value { get; } + string Key { get; } + string Value { get; } } public class AudioFilterOptions { - public List Arguments { get; } = new List(); + 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)); @@ -50,6 +50,17 @@ public AudioFilterOptions DynamicNormalizer(int frameLength = 500, int filterWin 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) { @@ -57,4 +68,4 @@ private AudioFilterOptions WithArgument(IAudioFilterArgument argument) return this; } } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Arguments/AudioGateArgument.cs b/FFMpegCore/FFMpeg/Arguments/AudioGateArgument.cs new file mode 100644 index 0000000..5b7b405 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/AudioGateArgument.cs @@ -0,0 +1,98 @@ +using System.Globalization; + +namespace FFMpegCore.Arguments +{ + 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") + { + 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); + } + + 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 6f1365d..79e8a39 100644 --- a/FFMpegCore/FFMpeg/Arguments/AudioSamplingRateArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/AudioSamplingRateArgument.cs @@ -13,4 +13,4 @@ public AudioSamplingRateArgument(int samplingRate = 48000) public string Text => $"-ar {SamplingRate}"; } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Arguments/BlackDetectArgument.cs b/FFMpegCore/FFMpeg/Arguments/BlackDetectArgument.cs new file mode 100644 index 0000000..ee088be --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/BlackDetectArgument.cs @@ -0,0 +1,14 @@ +namespace FFMpegCore.Arguments +{ + public class BlackDetectArgument : IVideoFilterArgument + { + 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}"; + } + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/BlackFrameArgument.cs b/FFMpegCore/FFMpeg/Arguments/BlackFrameArgument.cs new file mode 100644 index 0000000..9d5bad6 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/BlackFrameArgument.cs @@ -0,0 +1,14 @@ +namespace FFMpegCore.Arguments +{ + internal class BlackFrameArgument : IVideoFilterArgument + { + public string Key => "blackframe"; + + public string Value { get; } + + public BlackFrameArgument(int amount = 98, int threshold = 32) + { + Value = $"amount={amount}:threshold={threshold}"; + } + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/ConcatArgument.cs b/FFMpegCore/FFMpeg/Arguments/ConcatArgument.cs index 9c6ffa2..209bcb0 100644 --- a/FFMpegCore/FFMpeg/Arguments/ConcatArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/ConcatArgument.cs @@ -1,8 +1,4 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments { /// @@ -20,7 +16,7 @@ public ConcatArgument(IEnumerable values) public void Pre() { } public Task During(CancellationToken cancellationToken = default) => 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 c02cfa3..2736f60 100644 --- a/FFMpegCore/FFMpeg/Arguments/ConstantRateFactorArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/ConstantRateFactorArgument.cs @@ -1,6 +1,4 @@ -using System; - -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments { /// /// Constant Rate Factor (CRF) argument @@ -21,4 +19,4 @@ public ConstantRateFactorArgument(int crf) public string Text => $"-crf {Crf}"; } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Arguments/CopyArgument.cs b/FFMpegCore/FFMpeg/Arguments/CopyArgument.cs index 91419d5..eeac4c8 100644 --- a/FFMpegCore/FFMpeg/Arguments/CopyArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/CopyArgument.cs @@ -16,9 +16,8 @@ public CopyArgument(Channel channel = Channel.Both) public string Text => Channel switch { - Channel.Audio => "-c:a copy", - Channel.Video => "-c:v copy", - _ => "-c copy" + Channel.Both => "-c:a copy -c:v copy", + _ => $"-c{Channel.StreamType()} copy" }; } } diff --git a/FFMpegCore/FFMpeg/Arguments/CustomArgument.cs b/FFMpegCore/FFMpeg/Arguments/CustomArgument.cs index 8eedb12..c6dc85d 100644 --- a/FFMpegCore/FFMpeg/Arguments/CustomArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/CustomArgument.cs @@ -11,4 +11,4 @@ public CustomArgument(string argument) public string Text => Argument ?? string.Empty; } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs b/FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs index 47564f9..806afd0 100644 --- a/FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/DemuxConcatArgument.cs @@ -1,11 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments { /// /// Represents parameter of concat argument @@ -26,7 +19,7 @@ public DemuxConcatArgument(IEnumerable values) /// /// private string Escape(string value) => value.Replace("'", @"'\''"); - + private readonly string _tempFileName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, $"concat_{Guid.NewGuid()}.txt"); public void Pre() => File.WriteAllLines(_tempFileName, Values); diff --git a/FFMpegCore/FFMpeg/Arguments/DisableChannelArgument.cs b/FFMpegCore/FFMpeg/Arguments/DisableChannelArgument.cs index d683775..b6dd918 100644 --- a/FFMpegCore/FFMpeg/Arguments/DisableChannelArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/DisableChannelArgument.cs @@ -13,7 +13,10 @@ public class DisableChannelArgument : IArgument public DisableChannelArgument(Channel channel) { if (channel == Channel.Both) + { throw new FFMpegException(FFMpegExceptionType.Conversion, "Cannot disable both channels"); + } + Channel = channel; } diff --git a/FFMpegCore/FFMpeg/Arguments/DrawTextArgument.cs b/FFMpegCore/FFMpeg/Arguments/DrawTextArgument.cs index c148328..11f240b 100644 --- a/FFMpegCore/FFMpeg/Arguments/DrawTextArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/DrawTextArgument.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using System.Linq; - -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments { /// /// Drawtext video filter argument @@ -9,16 +6,16 @@ namespace FFMpegCore.Arguments public class DrawTextArgument : IVideoFilterArgument { public readonly DrawTextOptions Options; - + public DrawTextArgument(DrawTextOptions options) { Options = options; } - + public string Key { get; } = "drawtext"; public string Value => Options.TextInternal; } - + public class DrawTextOptions { public readonly string Text; @@ -34,7 +31,7 @@ public static DrawTextOptions Create(string text, string font, params (string ke 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)); private static string FormatArgumentPair((string key, string value) pair) { @@ -59,4 +56,4 @@ public DrawTextOptions WithParameter(string key, string value) return this; } } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Arguments/DurationArgument.cs b/FFMpegCore/FFMpeg/Arguments/DurationArgument.cs index e47b966..f1da817 100644 --- a/FFMpegCore/FFMpeg/Arguments/DurationArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/DurationArgument.cs @@ -1,6 +1,4 @@ -using System; - -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments { /// /// Represents duration parameter diff --git a/FFMpegCore/FFMpeg/Arguments/DynamicNormalizerArgument.cs b/FFMpegCore/FFMpeg/Arguments/DynamicNormalizerArgument.cs index d1c948c..518afee 100644 --- a/FFMpegCore/FFMpeg/Arguments/DynamicNormalizerArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/DynamicNormalizerArgument.cs @@ -1,13 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; +using System.Globalization; namespace FFMpegCore.Arguments { public class DynamicNormalizerArgument : IAudioFilterArgument { - private readonly Dictionary _arguments = new Dictionary(); + private readonly Dictionary _arguments = new(); /// /// Dynamic Audio Normalizer. @@ -23,13 +20,40 @@ public class DynamicNormalizerArgument : IAudioFilterArgument /// 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) 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"); + 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()); @@ -46,4 +70,4 @@ public DynamicNormalizerArgument(int frameLength = 500, int filterWindow = 31, d public string Value => string.Join(":", _arguments.Select(pair => $"{pair.Key}={pair.Value}")); } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Arguments/FaststartArgument.cs b/FFMpegCore/FFMpeg/Arguments/FaststartArgument.cs index 54cdd6f..185d67e 100644 --- a/FFMpegCore/FFMpeg/Arguments/FaststartArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/FaststartArgument.cs @@ -7,4 +7,4 @@ public class FaststartArgument : IArgument { public string Text => "-movflags faststart"; } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Arguments/HardwareAccelerationArgument.cs b/FFMpegCore/FFMpeg/Arguments/HardwareAccelerationArgument.cs index da4b9ee..d447879 100644 --- a/FFMpegCore/FFMpeg/Arguments/HardwareAccelerationArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/HardwareAccelerationArgument.cs @@ -11,8 +11,6 @@ public HardwareAccelerationArgument(HardwareAccelerationDevice hardwareAccelerat HardwareAccelerationDevice = hardwareAccelerationDevice; } - public string Text => HardwareAccelerationDevice != HardwareAccelerationDevice.Auto - ? $"-hwaccel {HardwareAccelerationDevice.ToString().ToLower()}" - : "-hwaccel"; + public string Text => $"-hwaccel {HardwareAccelerationDevice.ToString().ToLower()}"; } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Arguments/HighPassFilterArgument.cs b/FFMpegCore/FFMpeg/Arguments/HighPassFilterArgument.cs new file mode 100644 index 0000000..cf7ccfd --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/HighPassFilterArgument.cs @@ -0,0 +1,78 @@ +using System.Globalization; + +namespace FFMpegCore.Arguments +{ + public class HighPassFilterArgument : IAudioFilterArgument + { + 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) + { + 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()); + } + } + + 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 2a6c11a..2416b5d 100644 --- a/FFMpegCore/FFMpeg/Arguments/IArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/IArgument.cs @@ -7,4 +7,4 @@ public interface IArgument /// string Text { get; } } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Arguments/IDynamicArgument.cs b/FFMpegCore/FFMpeg/Arguments/IDynamicArgument.cs index 36a504e..d44d18d 100644 --- a/FFMpegCore/FFMpeg/Arguments/IDynamicArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/IDynamicArgument.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using System.Text; - -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments { public interface IDynamicArgument { @@ -13,4 +10,4 @@ public interface IDynamicArgument //public string GetText(StringBuilder context); public string GetText(IEnumerable context); } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Arguments/IInputArgument.cs b/FFMpegCore/FFMpeg/Arguments/IInputArgument.cs index 81a1cbe..c4a9e3c 100644 --- a/FFMpegCore/FFMpeg/Arguments/IInputArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/IInputArgument.cs @@ -3,4 +3,4 @@ public interface IInputArgument : IInputOutputArgument { } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Arguments/IInputOutputArgument.cs b/FFMpegCore/FFMpeg/Arguments/IInputOutputArgument.cs index 99def82..b925b58 100644 --- a/FFMpegCore/FFMpeg/Arguments/IInputOutputArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/IInputOutputArgument.cs @@ -1,7 +1,4 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments { public interface IInputOutputArgument : IArgument { @@ -9,4 +6,4 @@ public interface IInputOutputArgument : IArgument Task During(CancellationToken cancellationToken = default); void Post(); } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Arguments/IOutputArgument.cs b/FFMpegCore/FFMpeg/Arguments/IOutputArgument.cs index 09ccc83..590c819 100644 --- a/FFMpegCore/FFMpeg/Arguments/IOutputArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/IOutputArgument.cs @@ -3,4 +3,4 @@ public interface IOutputArgument : IInputOutputArgument { } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Arguments/InputArgument.cs b/FFMpegCore/FFMpeg/Arguments/InputArgument.cs index 68c34b4..4d6f123 100644 --- a/FFMpegCore/FFMpeg/Arguments/InputArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/InputArgument.cs @@ -1,8 +1,4 @@ -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments { /// /// Represents input parameter @@ -11,7 +7,7 @@ public class InputArgument : IInputArgument { public readonly bool VerifyExists; public readonly string FilePath; - + public InputArgument(bool verifyExists, string filePaths) { VerifyExists = verifyExists; @@ -23,12 +19,14 @@ 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}\""; } } diff --git a/FFMpegCore/FFMpeg/Arguments/InputDeviceArgument.cs b/FFMpegCore/FFMpeg/Arguments/InputDeviceArgument.cs index f276bbb..c6fecd2 100644 --- a/FFMpegCore/FFMpeg/Arguments/InputDeviceArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/InputDeviceArgument.cs @@ -1,7 +1,4 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments { /// /// Represents an input device parameter diff --git a/FFMpegCore/FFMpeg/Arguments/InputPipeArgument.cs b/FFMpegCore/FFMpeg/Arguments/InputPipeArgument.cs index d5ba44c..d28133f 100644 --- a/FFMpegCore/FFMpeg/Arguments/InputPipeArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/InputPipeArgument.cs @@ -1,7 +1,4 @@ -using System; -using System.IO.Pipes; -using System.Threading; -using System.Threading.Tasks; +using System.IO.Pipes; using FFMpegCore.Pipes; namespace FFMpegCore.Arguments @@ -24,7 +21,10 @@ 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); } } diff --git a/FFMpegCore/FFMpeg/Arguments/LowPassFilterArgument.cs b/FFMpegCore/FFMpeg/Arguments/LowPassFilterArgument.cs new file mode 100644 index 0000000..add0209 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/LowPassFilterArgument.cs @@ -0,0 +1,78 @@ +using System.Globalization; + +namespace FFMpegCore.Arguments +{ + public class LowPassFilterArgument : IAudioFilterArgument + { + 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) + { + 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()); + } + } + + 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 afec731..218de1b 100644 --- a/FFMpegCore/FFMpeg/Arguments/MapMetadataArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/MapMetadataArgument.cs @@ -1,13 +1,4 @@ -using FFMpegCore.Extend; - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments { public class MapMetadataArgument : IInputArgument, IDynamicArgument { @@ -58,7 +49,5 @@ public void Post() public void Pre() { } - - } } diff --git a/FFMpegCore/FFMpeg/Arguments/MapStreamArgument.cs b/FFMpegCore/FFMpeg/Arguments/MapStreamArgument.cs index b904be5..2c1c5fd 100644 --- a/FFMpegCore/FFMpeg/Arguments/MapStreamArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/MapStreamArgument.cs @@ -1,19 +1,31 @@ -namespace FFMpegCore.Arguments +using FFMpegCore.Enums; + +namespace FFMpegCore.Arguments { /// - /// Represents choice of video stream + /// Represents choice of stream by the stream specifier /// public class MapStreamArgument : IArgument { private readonly int _inputFileIndex; private readonly int _streamIndex; + private readonly Channel _channel; + private readonly bool _negativeMap; - public MapStreamArgument(int streamIndex, int inputFileIndex) + public MapStreamArgument(int streamIndex, int inputFileIndex, Channel channel = Channel.All, bool negativeMap = false) { + if (channel == Channel.Both) + { + // "Both" is not valid in this case and probably means all stream types + channel = Channel.All; + } + _inputFileIndex = inputFileIndex; _streamIndex = streamIndex; + _channel = channel; + _negativeMap = negativeMap; } - public string Text => $"-map {_inputFileIndex}:{_streamIndex}"; + public string Text => $"-map {(_negativeMap ? "-" : "")}{_inputFileIndex}{_channel.StreamType()}:{_streamIndex}"; } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs b/FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs index 89bb1fe..02e3f7a 100644 --- a/FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs @@ -1,14 +1,4 @@ -using FFMpegCore.Extend; - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments { public class MetaDataArgument : IInputArgument, IDynamicArgument { @@ -24,7 +14,6 @@ public MetaDataArgument(string metaDataContent) public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask; - public void Pre() => File.WriteAllText(_tempFileName, _metaDataContent); public void Post() => File.Delete(_tempFileName); diff --git a/FFMpegCore/FFMpeg/Arguments/OutputArgument.cs b/FFMpegCore/FFMpeg/Arguments/OutputArgument.cs index c2aad38..d5793aa 100644 --- a/FFMpegCore/FFMpeg/Arguments/OutputArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/OutputArgument.cs @@ -1,8 +1,4 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using FFMpegCore.Exceptions; +using FFMpegCore.Exceptions; namespace FFMpegCore.Arguments { @@ -23,7 +19,9 @@ public OutputArgument(string path, bool overwrite = true) 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() diff --git a/FFMpegCore/FFMpeg/Arguments/OutputPipeArgument.cs b/FFMpegCore/FFMpeg/Arguments/OutputPipeArgument.cs index f089a1e..d21cc04 100644 --- a/FFMpegCore/FFMpeg/Arguments/OutputPipeArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/OutputPipeArgument.cs @@ -1,6 +1,4 @@ using System.IO.Pipes; -using System.Threading; -using System.Threading.Tasks; using FFMpegCore.Pipes; namespace FFMpegCore.Arguments @@ -20,7 +18,10 @@ 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); } } diff --git a/FFMpegCore/FFMpeg/Arguments/OutputUrlArgument.cs b/FFMpegCore/FFMpeg/Arguments/OutputUrlArgument.cs index 15cbef9..36107c4 100644 --- a/FFMpegCore/FFMpeg/Arguments/OutputUrlArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/OutputUrlArgument.cs @@ -1,7 +1,4 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments { /// /// Represents outputting to url using supported protocols diff --git a/FFMpegCore/FFMpeg/Arguments/PadArgument.cs b/FFMpegCore/FFMpeg/Arguments/PadArgument.cs new file mode 100644 index 0000000..66dcb28 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/PadArgument.cs @@ -0,0 +1,64 @@ +using FFMpegCore.Extend; + +namespace FFMpegCore.Arguments +{ + public class PadArgument : IVideoFilterArgument + { + private readonly PadOptions _options; + + public PadArgument(PadOptions options) + { + _options = options; + } + + public string Key => "pad"; + public string Value => _options.TextInternal; + + } + + public class PadOptions + { + public readonly Dictionary Parameters = new(); + + internal string TextInternal => string.Join(":", Parameters.Select(parameter => parameter.FormatArgumentPair(true))); + + public static PadOptions Create(string? width, string? height) + { + return new PadOptions(width, height); + } + + public static PadOptions Create(string aspectRatio) + { + return new PadOptions(aspectRatio); + } + + public PadOptions WithParameter(string key, string value) + { + Parameters.Add(key, value); + return this; + } + + private PadOptions(string? width, string? height) + { + if (width == null && height == null) + { + throw new Exception("At least one of the parameters must be not null"); + } + + if (width != null) + { + Parameters.Add("width", width); + } + + if (height != null) + { + Parameters.Add("height", height); + } + } + + private PadOptions(string aspectRatio) + { + Parameters.Add("aspect", aspectRatio); + } + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/PanArgument.cs b/FFMpegCore/FFMpeg/Arguments/PanArgument.cs index 013fbf6..636ad73 100644 --- a/FFMpegCore/FFMpeg/Arguments/PanArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/PanArgument.cs @@ -1,7 +1,4 @@ -using System; -using System.Linq; - -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments { /// /// Mix channels with specific gain levels. @@ -24,11 +21,11 @@ public PanArgument(string channelLayout, params string[] outputDefinitions) { if (string.IsNullOrWhiteSpace(channelLayout)) { - throw new ArgumentException("The channel layout must be set" ,nameof(channelLayout)); + throw new ArgumentException("The channel layout must be set", nameof(channelLayout)); } ChannelLayout = channelLayout; - + _outputDefinitions = outputDefinitions; } @@ -41,11 +38,16 @@ public PanArgument(string channelLayout, params string[] outputDefinitions) /// public PanArgument(int channels, params string[] outputDefinitions) { - if (channels <= 0) throw new ArgumentOutOfRangeException(nameof(channels)); - + 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; diff --git a/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs b/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs index ddaab82..0751c9e 100644 --- a/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs @@ -1,8 +1,5 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; using System.IO.Pipes; -using System.Threading; -using System.Threading.Tasks; using FFMpegCore.Pipes; namespace FFMpegCore.Arguments @@ -24,7 +21,9 @@ protected PipeArgument(PipeDirection direction) public void Pre() { if (Pipe != null) + { throw new InvalidOperationException("Pipe already has been opened"); + } Pipe = new NamedPipeServerStream(PipeName, _direction, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); } @@ -40,7 +39,7 @@ public async Task During(CancellationToken cancellationToken = default) { try { - await ProcessDataAsync(cancellationToken).ConfigureAwait(false); + await ProcessDataAsync(cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -50,7 +49,9 @@ public async Task During(CancellationToken cancellationToken = default) { Debug.WriteLine($"Disconnecting NamedPipeServerStream on {GetType().Name}"); if (Pipe is { IsConnected: true }) + { Pipe.Disconnect(); + } } } diff --git a/FFMpegCore/FFMpeg/Arguments/RemoveMetadataArgument.cs b/FFMpegCore/FFMpeg/Arguments/RemoveMetadataArgument.cs index 29cdac6..53cebad 100644 --- a/FFMpegCore/FFMpeg/Arguments/RemoveMetadataArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/RemoveMetadataArgument.cs @@ -7,4 +7,4 @@ public class RemoveMetadataArgument : IArgument { public string Text => "-map_metadata -1"; } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Arguments/SeekArgument.cs b/FFMpegCore/FFMpeg/Arguments/SeekArgument.cs index 1b58890..8862e76 100644 --- a/FFMpegCore/FFMpeg/Arguments/SeekArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/SeekArgument.cs @@ -1,6 +1,4 @@ -using System; - -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments { /// /// Represents seek parameter @@ -14,15 +12,18 @@ public SeekArgument(TimeSpan? seekTo) SeekTo = seekTo; } - public string Text { - get { - if(SeekTo.HasValue) + public string Text + { + get + { + if (SeekTo.HasValue) { - int hours = SeekTo.Value.Hours; - if(SeekTo.Value.Days > 0) + var hours = SeekTo.Value.Hours; + if (SeekTo.Value.Days > 0) { hours += SeekTo.Value.Days * 24; } + return $"-ss {hours.ToString("00")}:{SeekTo.Value.Minutes.ToString("00")}:{SeekTo.Value.Seconds.ToString("00")}.{SeekTo.Value.Milliseconds.ToString("000")}"; } else diff --git a/FFMpegCore/FFMpeg/Arguments/SetMirroringArgument.cs b/FFMpegCore/FFMpeg/Arguments/SetMirroringArgument.cs index fff98f3..f384cb7 100644 --- a/FFMpegCore/FFMpeg/Arguments/SetMirroringArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/SetMirroringArgument.cs @@ -1,5 +1,4 @@ using FFMpegCore.Enums; -using System; namespace FFMpegCore.Arguments { @@ -14,17 +13,12 @@ public SetMirroringArgument(Mirroring mirroring) public string Key => string.Empty; - public string Value - { - get + public string Value => + Mirroring switch { - return Mirroring switch - { - Mirroring.Horizontal => "hflip", - Mirroring.Vertical => "vflip", - _ => throw new ArgumentOutOfRangeException(nameof(Mirroring)) - }; - } - } + Mirroring.Horizontal => "hflip", + Mirroring.Vertical => "vflip", + _ => throw new ArgumentOutOfRangeException(nameof(Mirroring)) + }; } } diff --git a/FFMpegCore/FFMpeg/Arguments/SilenceDetectArgument.cs b/FFMpegCore/FFMpeg/Arguments/SilenceDetectArgument.cs new file mode 100644 index 0000000..23c6ba3 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/SilenceDetectArgument.cs @@ -0,0 +1,39 @@ +using System.Globalization; + +namespace FFMpegCore.Arguments +{ + 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) + { + 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()); + } + + public string Key { get; } = "silencedetect"; + + public string Value => string.Join(":", _arguments.Select(pair => $"{pair.Key}={pair.Value}")); + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/StartNumberArgument.cs b/FFMpegCore/FFMpeg/Arguments/StartNumberArgument.cs index f7c09da..8e205af 100644 --- a/FFMpegCore/FFMpeg/Arguments/StartNumberArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/StartNumberArgument.cs @@ -6,7 +6,7 @@ public class StartNumberArgument : IArgument { public readonly int StartNumber; - + public StartNumberArgument(int startNumber) { StartNumber = startNumber; diff --git a/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs b/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs index 2acd7ca..85db8ae 100644 --- a/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/SubtitleHardBurnArgument.cs @@ -1,7 +1,5 @@ -using FFMpegCore.Extend; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; +using System.Drawing; +using FFMpegCore.Extend; namespace FFMpegCore.Arguments { @@ -23,7 +21,7 @@ public class SubtitleHardBurnOptions { private readonly string _subtitle; - public readonly Dictionary Parameters = new Dictionary(); + public readonly Dictionary Parameters = new(); /// /// Create a new using a provided subtitle file or a video file @@ -110,7 +108,7 @@ public SubtitleHardBurnOptions WithParameter(string key, string value) public class StyleOptions { - public readonly Dictionary Parameters = new Dictionary(); + public readonly Dictionary Parameters = new(); public static StyleOptions Create() { @@ -131,4 +129,4 @@ public StyleOptions WithParameter(string key, string value) internal string TextInternal => string.Join(",", Parameters.Select(parameter => parameter.FormatArgumentPair(enclose: false))); } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Arguments/ThreadsArgument.cs b/FFMpegCore/FFMpeg/Arguments/ThreadsArgument.cs index 6fd94e6..5e1a208 100644 --- a/FFMpegCore/FFMpeg/Arguments/ThreadsArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/ThreadsArgument.cs @@ -1,6 +1,4 @@ -using System; - -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments { /// /// Represents threads parameter diff --git a/FFMpegCore/FFMpeg/Arguments/TransposeArgument.cs b/FFMpegCore/FFMpeg/Arguments/TransposeArgument.cs index bd15c47..e111060 100644 --- a/FFMpegCore/FFMpeg/Arguments/TransposeArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/TransposeArgument.cs @@ -20,4 +20,4 @@ public TransposeArgument(Transposition transposition) public string Key { get; } = "transpose"; public string Value => ((int)Transposition).ToString(); } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Arguments/VariableBitRateArgument.cs b/FFMpegCore/FFMpeg/Arguments/VariableBitRateArgument.cs index b656ec4..2f169d8 100644 --- a/FFMpegCore/FFMpeg/Arguments/VariableBitRateArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/VariableBitRateArgument.cs @@ -1,6 +1,4 @@ -using System; - -namespace FFMpegCore.Arguments +namespace FFMpegCore.Arguments { /// /// Variable Bitrate Argument (VBR) argument @@ -21,4 +19,4 @@ public VariableBitRateArgument(int vbr) public string Text => $"-vbr {Vbr}"; } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Arguments/VerbosityLevelArgument.cs b/FFMpegCore/FFMpeg/Arguments/VerbosityLevelArgument.cs index f128aeb..da236f9 100644 --- a/FFMpegCore/FFMpeg/Arguments/VerbosityLevelArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/VerbosityLevelArgument.cs @@ -22,4 +22,4 @@ public enum VerbosityLevel Debug = 48, Trace = 56 } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Arguments/VideoBitrateArgument.cs b/FFMpegCore/FFMpeg/Arguments/VideoBitrateArgument.cs index ea5e641..213b3d1 100644 --- a/FFMpegCore/FFMpeg/Arguments/VideoBitrateArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/VideoBitrateArgument.cs @@ -14,4 +14,4 @@ public VideoBitrateArgument(int bitrate) public string Text => $"-b:v {Bitrate}k"; } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Arguments/VideoCodecArgument.cs b/FFMpegCore/FFMpeg/Arguments/VideoCodecArgument.cs index 9386822..b12afc7 100644 --- a/FFMpegCore/FFMpeg/Arguments/VideoCodecArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/VideoCodecArgument.cs @@ -15,10 +15,12 @@ public VideoCodecArgument(string codec) Codec = codec; } - public VideoCodecArgument(Codec value) + 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; } diff --git a/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs b/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs index 4d0dfde..f7a9e4a 100644 --- a/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; -using System.Drawing; -using System.Linq; +using System.Drawing; using FFMpegCore.Enums; using FFMpegCore.Exceptions; @@ -9,7 +7,7 @@ namespace FFMpegCore.Arguments public class VideoFiltersArgument : IArgument { public readonly VideoFilterOptions Options; - + public VideoFiltersArgument(VideoFilterOptions options) { Options = options; @@ -20,7 +18,9 @@ public VideoFiltersArgument(VideoFilterOptions options) private string GetText() { if (!Options.Arguments.Any()) + { throw new FFMpegArgumentException("No video-filter arguments provided"); + } var arguments = Options.Arguments .Where(arg => !string.IsNullOrEmpty(arg.Value)) @@ -42,8 +42,8 @@ public interface IVideoFilterArgument public class VideoFilterOptions { - public List Arguments { get; } = new List(); - + 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)); @@ -51,6 +51,9 @@ public class VideoFilterOptions 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) { @@ -58,4 +61,4 @@ private VideoFilterOptions WithArgument(IVideoFilterArgument argument) return this; } } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Builders/MetaData/ChapterData.cs b/FFMpegCore/FFMpeg/Builders/MetaData/ChapterData.cs index 24ad2b6..77a47bd 100644 --- a/FFMpegCore/FFMpeg/Builders/MetaData/ChapterData.cs +++ b/FFMpegCore/FFMpeg/Builders/MetaData/ChapterData.cs @@ -1,6 +1,4 @@ -using System; - -namespace FFMpegCore.Builders.MetaData +namespace FFMpegCore.Builders.MetaData { public class ChapterData { @@ -15,4 +13,4 @@ public ChapterData(string title, TimeSpan start, TimeSpan end) End = end; } } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Builders/MetaData/IReadOnlyMetaData.cs b/FFMpegCore/FFMpeg/Builders/MetaData/IReadOnlyMetaData.cs index fd55ea7..ced87d2 100644 --- a/FFMpegCore/FFMpeg/Builders/MetaData/IReadOnlyMetaData.cs +++ b/FFMpegCore/FFMpeg/Builders/MetaData/IReadOnlyMetaData.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace FFMpegCore.Builders.MetaData +namespace FFMpegCore.Builders.MetaData { public interface IReadOnlyMetaData @@ -8,4 +6,4 @@ public interface IReadOnlyMetaData IReadOnlyList Chapters { get; } IReadOnlyDictionary Entries { get; } } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Builders/MetaData/MetaData.cs b/FFMpegCore/FFMpeg/Builders/MetaData/MetaData.cs index 2efc696..e8fdd42 100644 --- a/FFMpegCore/FFMpeg/Builders/MetaData/MetaData.cs +++ b/FFMpegCore/FFMpeg/Builders/MetaData/MetaData.cs @@ -1,15 +1,12 @@ -using System.Collections.Generic; -using System.Linq; - -namespace FFMpegCore.Builders.MetaData +namespace FFMpegCore.Builders.MetaData { public class MetaData : IReadOnlyMetaData { public Dictionary Entries { get; private set; } public List Chapters { get; private set; } - IReadOnlyList IReadOnlyMetaData.Chapters => this.Chapters; - IReadOnlyDictionary IReadOnlyMetaData.Entries => this.Entries; + IReadOnlyList IReadOnlyMetaData.Chapters => Chapters; + IReadOnlyDictionary IReadOnlyMetaData.Entries => Entries; public MetaData() { @@ -30,4 +27,4 @@ public MetaData(MetaData cloneSource) .ToList(); } } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs b/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs index 29c13c2..615512b 100644 --- a/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs +++ b/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataBuilder.cs @@ -1,18 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace FFMpegCore.Builders.MetaData +namespace FFMpegCore.Builders.MetaData { public class MetaDataBuilder { - private MetaData _metaData = new MetaData(); + private readonly MetaData _metaData = new(); public MetaDataBuilder WithEntry(string key, string entry) { if (_metaData.Entries.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value)) { - entry = String.Concat(value, "; ", entry); + entry = string.Concat(value, "; ", entry); } _metaData.Entries[key] = entry; @@ -20,10 +16,10 @@ public MetaDataBuilder WithEntry(string key, string entry) } public MetaDataBuilder WithEntry(string key, params string[] values) - => this.WithEntry(key, String.Join("; ", values)); + => WithEntry(key, string.Join("; ", values)); public MetaDataBuilder WithEntry(string key, IEnumerable values) - => this.WithEntry(key, String.Join("; ", values)); + => WithEntry(key, string.Join("; ", values)); public MetaDataBuilder AddChapter(ChapterData chapterData) { @@ -33,7 +29,7 @@ public MetaDataBuilder AddChapter(ChapterData chapterData) public MetaDataBuilder AddChapters(IEnumerable values, Func chapterGetter) { - foreach (T value in values) + foreach (var value in values) { var (duration, title) = chapterGetter(value); AddChapter(duration, title); @@ -46,13 +42,13 @@ 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; + title = string.IsNullOrEmpty(title) ? $"Chapter {_metaData.Chapters.Count + 1}" : title; _metaData.Chapters.Add(new ChapterData ( start: start, end: end, - title: title ?? String.Empty + title: title ?? string.Empty )); return this; @@ -102,8 +98,6 @@ public MetaDataBuilder AddChapter(TimeSpan duration, string? title = null) //encoder=Lavf58.47.100 public MetaDataBuilder WithEncoder(string value) => WithEntry("encoder", value); - - - public ReadOnlyMetaData Build() => new ReadOnlyMetaData(_metaData); + public ReadOnlyMetaData Build() => new(_metaData); } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataSerializer.cs b/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataSerializer.cs index 1a6f176..8db1876 100644 --- a/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataSerializer.cs +++ b/FFMpegCore/FFMpeg/Builders/MetaData/MetaDataSerializer.cs @@ -1,11 +1,10 @@ -using System.Linq; -using System.Text; +using System.Text; namespace FFMpegCore.Builders.MetaData { public class MetaDataSerializer { - public static readonly MetaDataSerializer Instance = new MetaDataSerializer(); + public static readonly MetaDataSerializer Instance = new(); public string Serialize(IReadOnlyMetaData metaData) { @@ -17,7 +16,7 @@ public string Serialize(IReadOnlyMetaData metaData) sb.AppendLine($"{value.Key}={value.Value}"); } - int chapterNumber = 0; + var chapterNumber = 0; foreach (var chapter in metaData.Chapters ?? Enumerable.Empty()) { chapterNumber++; @@ -35,4 +34,4 @@ public string Serialize(IReadOnlyMetaData metaData) return sb.ToString(); } } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Builders/MetaData/ReadOnlyMetaData.cs b/FFMpegCore/FFMpeg/Builders/MetaData/ReadOnlyMetaData.cs index ff9bae9..cf29f94 100644 --- a/FFMpegCore/FFMpeg/Builders/MetaData/ReadOnlyMetaData.cs +++ b/FFMpegCore/FFMpeg/Builders/MetaData/ReadOnlyMetaData.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using System.Linq; - -namespace FFMpegCore.Builders.MetaData +namespace FFMpegCore.Builders.MetaData { public class ReadOnlyMetaData : IReadOnlyMetaData { @@ -22,4 +19,4 @@ public ReadOnlyMetaData(MetaData metaData) .AsReadOnly(); } } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Enums/AudioQuality.cs b/FFMpegCore/FFMpeg/Enums/AudioQuality.cs index 60ba0eb..59ed3e6 100644 --- a/FFMpegCore/FFMpeg/Enums/AudioQuality.cs +++ b/FFMpegCore/FFMpeg/Enums/AudioQuality.cs @@ -9,4 +9,4 @@ public enum AudioQuality BelowNormal = 96, Low = 64 } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Enums/Codec.cs b/FFMpegCore/FFMpeg/Enums/Codec.cs index 8ac8456..efd4196 100644 --- a/FFMpegCore/FFMpeg/Enums/Codec.cs +++ b/FFMpegCore/FFMpeg/Enums/Codec.cs @@ -1,6 +1,5 @@ -using FFMpegCore.Exceptions; -using System; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; +using FFMpegCore.Exceptions; namespace FFMpegCore.Enums { @@ -13,8 +12,8 @@ public enum FeatureStatus public class Codec { - private static readonly Regex _codecsFormatRegex = new Regex(@"([D\.])([E\.])([VASD\.])([I\.])([L\.])([S\.])\s+([a-z0-9_-]+)\s+(.+)"); - private static readonly Regex _decodersEncodersFormatRegex = new Regex(@"([VASD\.])([F\.])([S\.])([X\.])([B\.])([D\.])\s+([a-z0-9_-]+)\s+(.+)"); + 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 { @@ -73,7 +72,7 @@ internal static bool TryParseFromCodecs(string line, out Codec codec) _ => CodecType.Unknown }; - if(type == CodecType.Unknown) + if (type == CodecType.Unknown) { codec = null!; return false; @@ -133,7 +132,9 @@ internal static bool TryParseFromEncodersDecoders(string line, out Codec codec, internal void Merge(Codec other) { if (Name != other.Name) + { throw new FFMpegException(FFMpegExceptionType.Operation, "different codecs enable to merge"); + } Type |= other.Type; DecodingSupported |= other.DecodingSupported; @@ -146,7 +147,9 @@ internal void Merge(Codec other) DecoderFeatureLevel.Merge(other.DecoderFeatureLevel); if (Description != other.Description) + { Description += "\r\n" + other.Description; + } } } } diff --git a/FFMpegCore/FFMpeg/Enums/ContainerFormat.cs b/FFMpegCore/FFMpeg/Enums/ContainerFormat.cs index 2da1572..53c5e1a 100644 --- a/FFMpegCore/FFMpeg/Enums/ContainerFormat.cs +++ b/FFMpegCore/FFMpeg/Enums/ContainerFormat.cs @@ -4,7 +4,7 @@ namespace FFMpegCore.Enums { public class ContainerFormat { - private static readonly Regex FormatRegex = new Regex(@"([D ])([E ])\s+([a-z0-9_]+)\s+(.+)"); + private static readonly Regex FormatRegex = new(@"([D ])([E ])\s+([a-z0-9_]+)\s+(.+)"); public string Name { get; private set; } public bool DemuxingSupported { get; private set; } @@ -16,7 +16,10 @@ public string Extension get { if (GlobalFFOptions.Current.ExtensionOverrides.ContainsKey(Name)) + { return GlobalFFOptions.Current.ExtensionOverrides[Name]; + } + return "." + Name; } } @@ -44,4 +47,4 @@ internal static bool TryParse(string line, out ContainerFormat fmt) return true; } } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Enums/Enums.cs b/FFMpegCore/FFMpeg/Enums/Enums.cs index 7520fea..4974b44 100644 --- a/FFMpegCore/FFMpeg/Enums/Enums.cs +++ b/FFMpegCore/FFMpeg/Enums/Enums.cs @@ -26,7 +26,7 @@ public static class AudioCodec 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 LibMp3Lame => FFMpeg.GetCodec("libmp3lame"); } public static class VideoType @@ -46,10 +46,42 @@ public enum Filter 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 + Both, + VideoNoAttachedPic, + Subtitle, + Data, + Attachments, + All } -} \ No newline at end of file + 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 new file mode 100644 index 0000000..aa2ca23 --- /dev/null +++ b/FFMpegCore/FFMpeg/Enums/FFMpegLogLevel.cs @@ -0,0 +1,15 @@ +namespace FFMpegCore.Enums +{ + public enum FFMpegLogLevel + { + 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 d45faf6..b5e775d 100644 --- a/FFMpegCore/FFMpeg/Enums/FileExtension.cs +++ b/FFMpegCore/FFMpeg/Enums/FileExtension.cs @@ -1,6 +1,4 @@ -using System; - -namespace FFMpegCore.Enums +namespace FFMpegCore.Enums { public static class FileExtension { diff --git a/FFMpegCore/FFMpeg/Enums/HardwareAccelerationDevice.cs b/FFMpegCore/FFMpeg/Enums/HardwareAccelerationDevice.cs index 1d92f53..4c5dbd7 100644 --- a/FFMpegCore/FFMpeg/Enums/HardwareAccelerationDevice.cs +++ b/FFMpegCore/FFMpeg/Enums/HardwareAccelerationDevice.cs @@ -7,8 +7,9 @@ public enum HardwareAccelerationDevice DXVA2, QSV, CUVID, + CUDA, VDPAU, VAAPI, LibMFX } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Enums/PixelFormat.cs b/FFMpegCore/FFMpeg/Enums/PixelFormat.cs index 9808e43..0d89f4c 100644 --- a/FFMpegCore/FFMpeg/Enums/PixelFormat.cs +++ b/FFMpegCore/FFMpeg/Enums/PixelFormat.cs @@ -4,7 +4,7 @@ namespace FFMpegCore.Enums { public class PixelFormat { - private static readonly Regex _formatRegex = new Regex(@"([I\.])([O\.])([H\.])([P\.])([B\.])\s+(\S+)\s+([0-9]+)\s+([0-9]+)"); + private static readonly Regex _formatRegex = new(@"([I\.])([O\.])([H\.])([P\.])([B\.])\s+(\S+)\s+([0-9]+)\s+([0-9]+)"); public bool InputConversionSupported { get; private set; } public bool OutputConversionSupported { get; private set; } @@ -41,10 +41,16 @@ internal static bool TryParse(string line, out PixelFormat fmt) 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; diff --git a/FFMpegCore/FFMpeg/Enums/Speed.cs b/FFMpegCore/FFMpeg/Enums/Speed.cs index 52272f0..8400b56 100644 --- a/FFMpegCore/FFMpeg/Enums/Speed.cs +++ b/FFMpegCore/FFMpeg/Enums/Speed.cs @@ -12,4 +12,4 @@ public enum Speed SuperFast, UltraFast } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Enums/Transposition.cs b/FFMpegCore/FFMpeg/Enums/Transposition.cs index bacfccc..1e47bbb 100644 --- a/FFMpegCore/FFMpeg/Enums/Transposition.cs +++ b/FFMpegCore/FFMpeg/Enums/Transposition.cs @@ -7,4 +7,4 @@ public enum Transposition CounterClockwise90 = 2, Clockwise90VerticalFlip = 3 } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Enums/VideoSize.cs b/FFMpegCore/FFMpeg/Enums/VideoSize.cs index d774b95..29e203b 100644 --- a/FFMpegCore/FFMpeg/Enums/VideoSize.cs +++ b/FFMpegCore/FFMpeg/Enums/VideoSize.cs @@ -8,4 +8,4 @@ public enum VideoSize Ld = 360, Original = -1 } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs b/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs index 485cf20..e92ee56 100644 --- a/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs +++ b/FFMpegCore/FFMpeg/Exceptions/FFMpegException.cs @@ -1,6 +1,4 @@ -using System; - -namespace FFMpegCore.Exceptions +namespace FFMpegCore.Exceptions { public enum FFMpegExceptionType { @@ -30,7 +28,7 @@ public FFMpegException(FFMpegExceptionType type, string message) FFMpegErrorOutput = string.Empty; Type = type; } - + public FFMpegExceptionType Type { get; } public string FFMpegErrorOutput { get; } } @@ -57,4 +55,4 @@ public FFMpegStreamFormatException(FFMpegExceptionType type, string message, Exc { } } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index 9e9e0ce..58526b8 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -1,13 +1,7 @@ -using FFMpegCore.Enums; +using System.Drawing; +using FFMpegCore.Enums; using FFMpegCore.Exceptions; using FFMpegCore.Helpers; -using FFMpegCore.Pipes; -using System; -using System.Collections.Generic; -using System.Drawing; -using System.IO; -using System.Linq; -using System.Threading.Tasks; using Instances; namespace FFMpegCore @@ -27,10 +21,12 @@ public static class FFMpeg public static bool Snapshot(string input, string output, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0) { if (Path.GetExtension(output) != FileExtension.Png) - output = Path.GetFileNameWithoutExtension(output) + FileExtension.Png; + { + output = Path.Combine(Path.GetDirectoryName(output), Path.GetFileNameWithoutExtension(output) + FileExtension.Png); + } var source = FFProbe.Analyse(input); - var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); + var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); return arguments .OutputToFile(output, true, outputOptions) @@ -49,10 +45,12 @@ public static bool Snapshot(string input, string output, Size? size = null, Time public static async Task SnapshotAsync(string input, string output, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0) { if (Path.GetExtension(output) != FileExtension.Png) - output = Path.GetFileNameWithoutExtension(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) = BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); + var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); return await arguments .OutputToFile(output, true, outputOptions) @@ -60,110 +58,79 @@ public static async Task SnapshotAsync(string input, string output, Size? } /// - /// Saves a 'png' thumbnail to an in-memory bitmap + /// Converts an image sequence to a video. /// - /// 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) + /// Output video file. + /// FPS + /// Image sequence collection + /// Output video information. + public static bool JoinImageSequence(string output, double frameRate = 30, params string[] images) { - var source = FFProbe.Analyse(input); - var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); - using var ms = new MemoryStream(); - - 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); - } - /// - /// 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) = BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); - using var ms = new MemoryStream(); - - await arguments - .OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options - .ForceFormat("rawvideo"))) - .ProcessAsynchronously(); - - ms.Position = 0; - return new Bitmap(ms); - } - - private static (FFMpegArguments, Action outputOptions) BuildSnapshotArguments( - string input, - IMediaAnalysis source, - Size? size = null, - TimeSpan? captureTime = null, - int? streamIndex = null, - int inputFileIndex = 0) - { - captureTime ??= TimeSpan.FromSeconds(source.Duration.TotalSeconds / 3); - size = PrepareSnapshotSize(source, size); - streamIndex ??= source.PrimaryVideoStream?.Index - ?? source.VideoStreams.FirstOrDefault()?.Index - ?? 0; - - return (FFMpegArguments - .FromFileInput(input, false, options => options - .Seek(captureTime)), - options => options - .SelectStream((int)streamIndex, inputFileIndex) - .WithVideoCodec(VideoCodec.Png) - .WithFrameOutputCount(1) - .Resize(size)); - } - - private static Size? PrepareSnapshotSize(IMediaAnalysis source, Size? wantedSize) - { - if (wantedSize == null || (wantedSize.Value.Height <= 0 && wantedSize.Value.Width <= 0) || source.PrimaryVideoStream == null) - return null; - - var currentSize = new Size(source.PrimaryVideoStream.Width, source.PrimaryVideoStream.Height); - if (source.PrimaryVideoStream.Rotation == 90 || source.PrimaryVideoStream.Rotation == 180) - currentSize = new Size(source.PrimaryVideoStream.Height, source.PrimaryVideoStream.Width); - - if (wantedSize.Value.Width != currentSize.Width || wantedSize.Value.Height != currentSize.Height) + int? width = null, height = null; + var tempFolderName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, Guid.NewGuid().ToString()); + var temporaryImageFiles = images.Select((imagePath, index) => { - if (wantedSize.Value.Width <= 0 && wantedSize.Value.Height > 0) - { - var ratio = (double)wantedSize.Value.Height / currentSize.Height; - return new Size((int)(currentSize.Width * ratio), (int)(currentSize.Height * ratio)); - } - if (wantedSize.Value.Height <= 0 && wantedSize.Value.Width > 0) - { - var ratio = (double)wantedSize.Value.Width / currentSize.Width; - return new Size((int)(currentSize.Width * ratio), (int)(currentSize.Height * ratio)); - } - return wantedSize; - } + var analysis = FFProbe.Analyse(imagePath); + FFMpegHelper.ConversionSizeExceptionCheck(analysis.PrimaryVideoStream!.Width, analysis.PrimaryVideoStream!.Height); + width ??= analysis.PrimaryVideoStream.Width; + height ??= analysis.PrimaryVideoStream.Height; - return null; + var destinationPath = Path.Combine(tempFolderName, $"{index.ToString().PadLeft(9, '0')}{Path.GetExtension(imagePath)}"); + Directory.CreateDirectory(tempFolderName); + File.Copy(imagePath, destinationPath); + return destinationPath; + }).ToArray(); + + try + { + return FFMpegArguments + .FromFileInput(Path.Combine(tempFolderName, "%09d.png"), false) + .OutputToFile(output, true, options => options + .ForcePixelFormat("yuv420p") + .Resize(width!.Value, height!.Value) + .WithFramerate(frameRate)) + .ProcessSynchronously(); + } + finally + { + Cleanup(temporaryImageFiles); + Directory.Delete(tempFolderName); + } + } + + /// + /// 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. + /// Input video source. /// Output information. - /// Target conversion video type. + /// Target conversion video format. /// Conversion target speed/quality (faster speed = lower quality). /// Video size. /// Conversion target audio quality. @@ -186,7 +153,9 @@ public static bool Convert( 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 { @@ -196,7 +165,7 @@ public static bool Convert( .UsingMultithreading(multithreaded) .WithVideoCodec(VideoCodec.LibX264) .WithVideoBitrate(2400) - .WithVideoFilters(filterOptions => filterOptions + .WithVideoFilters(filterOptions => filterOptions .Scale(outputSize)) .WithSpeedPreset(speed) .WithAudioCodec(AudioCodec.Aac) @@ -208,7 +177,7 @@ public static bool Convert( .UsingMultithreading(multithreaded) .WithVideoCodec(VideoCodec.LibTheora) .WithVideoBitrate(2400) - .WithVideoFilters(filterOptions => filterOptions + .WithVideoFilters(filterOptions => filterOptions .Scale(outputSize)) .WithSpeedPreset(speed) .WithAudioCodec(AudioCodec.LibVorbis) @@ -227,7 +196,7 @@ public static bool Convert( .UsingMultithreading(multithreaded) .WithVideoCodec(VideoCodec.LibVpx) .WithVideoBitrate(2400) - .WithVideoFilters(filterOptions => filterOptions + .WithVideoFilters(filterOptions => filterOptions .Scale(outputSize)) .WithSpeedPreset(speed) .WithAudioCodec(AudioCodec.LibVorbis) @@ -237,35 +206,6 @@ public static bool Convert( }; } - /// - /// 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); - using (var imageFile = Image.FromFile(image)) - { - FFMpegHelper.ConversionSizeExceptionCheck(imageFile); - } - - 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(); - } - /// /// Joins a list of video files. /// @@ -299,44 +239,6 @@ public static bool Join(string output, params string[] videos) } } - /// - /// 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 ImageInfo[] images) - { - var tempFolderName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, Guid.NewGuid().ToString()); - var temporaryImageFiles = images.Select((imageInfo, index) => - { - using var image = Image.FromFile(imageInfo.FullName); - FFMpegHelper.ConversionSizeExceptionCheck(image); - var destinationPath = Path.Combine(tempFolderName, $"{index.ToString().PadLeft(9, '0')}{imageInfo.Extension}"); - Directory.CreateDirectory(tempFolderName); - File.Copy(imageInfo.FullName, destinationPath); - return destinationPath; - }).ToArray(); - - var firstImage = images.First(); - try - { - return FFMpegArguments - .FromFileInput(Path.Combine(tempFolderName, "%09d.png"), false) - .OutputToFile(output, true, options => options - .ForcePixelFormat("yuv420p") - .Resize(firstImage.Width, firstImage.Height) - .WithFramerate(frameRate)) - .ProcessSynchronously(); - } - finally - { - Cleanup(temporaryImageFiles); - Directory.Delete(tempFolderName); - } - } - /// /// Records M3U8 streams to the specified output. /// @@ -348,8 +250,10 @@ 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) .OutputToFile(output) @@ -428,12 +332,16 @@ internal static IReadOnlyList GetPixelFormatsInternal() processArguments.OutputDataReceived += (e, data) => { if (PixelFormat.TryParse(data, out var format)) + { list.Add(format); + } }; var result = processArguments.StartAndWaitForExit(); - if (result.ExitCode != 0) + if (result.ExitCode != 0) + { throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\r\n", result.OutputData)); + } return list.AsReadOnly(); } @@ -441,25 +349,33 @@ internal static IReadOnlyList GetPixelFormatsInternal() public static IReadOnlyList GetPixelFormats() { if (!GlobalFFOptions.Current.UseCache) + { return GetPixelFormatsInternal(); + } + return FFMpegCache.PixelFormats.Values.ToList().AsReadOnly(); } - public static bool TryGetPixelFormat(string name, out PixelFormat fmt) + public static bool TryGetPixelFormat(string name, out PixelFormat format) { if (!GlobalFFOptions.Current.UseCache) { - fmt = GetPixelFormatsInternal().FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim()); - return fmt != null; + format = GetPixelFormatsInternal().FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim()); + return format != null; } else - return FFMpegCache.PixelFormats.TryGetValue(name, out fmt); + { + 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 @@ -474,15 +390,24 @@ private static void ParsePartOfCodecs(Dictionary codecs, string a processArguments.OutputDataReceived += (e, data) => { var codec = parser(data); - if(codec != null) + if (codec != null) + { if (codecs.TryGetValue(codec.Name, out var parentCodec)) + { parentCodec.Merge(codec); + } else + { codecs.Add(codec.Name, codec); + } + } }; var result = processArguments.StartAndWaitForExit(); - if (result.ExitCode != 0) throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\r\n", result.OutputData)); + if (result.ExitCode != 0) + { + throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\r\n", result.OutputData)); + } } internal static Dictionary GetCodecsInternal() @@ -491,19 +416,28 @@ internal static Dictionary GetCodecsInternal() 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; }); @@ -513,15 +447,21 @@ internal static Dictionary GetCodecsInternal() 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(); + } + + return FFMpegCache.Codecs.Values.Where(x => x.Type == type).ToList().AsReadOnly(); } public static IReadOnlyList GetVideoCodecs() => GetCodecs(CodecType.Video); @@ -537,13 +477,18 @@ public static bool TryGetCodec(string name, out Codec codec) 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) + { return codec; + } + throw new FFMpegException(FFMpegExceptionType.Operation, $"Codec \"{name}\" not supported"); } #endregion @@ -558,11 +503,16 @@ internal static IReadOnlyList GetContainersFormatsInternal() 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)); + if (result.ExitCode != 0) + { + throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\r\n", result.OutputData)); + } return list.AsReadOnly(); } @@ -570,7 +520,10 @@ internal static IReadOnlyList GetContainersFormatsInternal() public static IReadOnlyList GetContainerFormats() { if (!GlobalFFOptions.Current.UseCache) + { return GetContainersFormatsInternal(); + } + return FFMpegCache.ContainerFormats.Values.ToList().AsReadOnly(); } @@ -582,13 +535,18 @@ public static bool TryGetContainerFormat(string name, out ContainerFormat fmt) 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 @@ -598,8 +556,10 @@ private static void Cleanup(IEnumerable pathList) foreach (var path in pathList) { if (File.Exists(path)) + { File.Delete(path); + } } } } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs index 7b3da7a..0f54b8c 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs @@ -1,6 +1,4 @@ -using System; -using System.Drawing; - +using System.Drawing; using FFMpegCore.Arguments; using FFMpegCore.Enums; @@ -19,8 +17,6 @@ internal FFMpegArgumentOptions() { } public FFMpegArgumentOptions Resize(int width, int height) => WithArgument(new SizeArgument(width, height)); public FFMpegArgumentOptions Resize(Size? size) => WithArgument(new SizeArgument(size)); - - 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)); @@ -60,7 +56,16 @@ public FFMpegArgumentOptions WithAudioFilters(Action audioFi public FFMpegArgumentOptions Seek(TimeSpan? seekTo) => WithArgument(new SeekArgument(seekTo)); public FFMpegArgumentOptions Loop(int times) => WithArgument(new LoopArgument(times)); public FFMpegArgumentOptions OverwriteExisting() => WithArgument(new OverwriteArgument()); - public FFMpegArgumentOptions SelectStream(int streamIndex, int inputFileIndex = 0) => WithArgument(new MapStreamArgument(streamIndex, inputFileIndex)); + 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, + (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, + (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)); @@ -71,11 +76,10 @@ public FFMpegArgumentOptions WithAudioFilters(Action audioFi public FFMpegArgumentOptions WithAudibleActivationBytes(string activationBytes) => WithArgument(new AudibleEncryptionKeyArgument(activationBytes)); public FFMpegArgumentOptions WithTagVersion(int id3v2Version = 3) => WithArgument(new ID3V2VersionArgument(id3v2Version)); - public FFMpegArgumentOptions WithArgument(IArgument argument) { Arguments.Add(argument); return this; } } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs index 43ace4d..27237d8 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs @@ -1,10 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; +using System.Diagnostics; using System.Globalization; using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; +using FFMpegCore.Enums; using FFMpegCore.Exceptions; using FFMpegCore.Helpers; using Instances; @@ -13,7 +10,7 @@ namespace FFMpegCore { public class FFMpegArgumentProcessor { - private static readonly Regex ProgressRegex = new Regex(@"time=(\d\d:\d\d:\d\d.\d\d?)", RegexOptions.Compiled); + 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; @@ -21,6 +18,7 @@ public class FFMpegArgumentProcessor private Action? _onOutput; private Action? _onError; private TimeSpan? _totalTimespan; + private FFMpegLogLevel? _logLevel; internal FFMpegArgumentProcessor(FFMpegArguments ffMpegArguments) { @@ -83,12 +81,23 @@ 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 { @@ -97,7 +106,9 @@ public bool ProcessSynchronously(bool throwOnError = true, FFOptions? ffMpegOpti catch (OperationCanceledException) { if (throwOnError) + { throw; + } } return HandleCompletion(throwOnError, processResult?.ExitCode ?? -1, processResult?.ErrorData ?? Array.Empty()); @@ -107,7 +118,7 @@ public async Task ProcessAsynchronously(bool throwOnError = true, FFOption { var options = GetConfiguredOptions(ffMpegOptions); var processArguments = PrepareProcessArguments(options, out var cancellationTokenSource); - + IProcessResult? processResult = null; try { @@ -116,9 +127,11 @@ public async Task ProcessAsynchronously(bool throwOnError = true, FFOption catch (OperationCanceledException) { if (throwOnError) + { throw; + } } - + return HandleCompletion(throwOnError, processResult?.ExitCode ?? -1, processResult?.ErrorData ?? Array.Empty()); } @@ -141,6 +154,7 @@ void OnCancelEvent(object sender, int timeout) instance.Kill(); } } + CancelEvent += OnCancelEvent; try @@ -168,10 +182,15 @@ await Task.WhenAll(instance.WaitForExitAsync().ContinueWith(t => 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); + if (_totalTimespan.HasValue) + { + _onTimeProgress?.Invoke(_totalTimespan.Value); + } return exitCode == 0; } @@ -193,10 +212,27 @@ private ProcessArguments PrepareProcessArguments(FFOptions ffOptions, { 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 = _ffMpegArguments.Text, + Arguments = arguments, StandardOutputEncoding = ffOptions.Encoding, StandardErrorEncoding = ffOptions.Encoding, WorkingDirectory = ffOptions.WorkingDirectory @@ -204,11 +240,15 @@ private ProcessArguments PrepareProcessArguments(FFOptions ffOptions, var processArguments = new ProcessArguments(startInfo); cancellationTokenSource = new CancellationTokenSource(); - if (_onOutput != null || _onTimeProgress != null || (_onPercentageProgress != null && _totalTimespan != null)) + if (_onOutput != null) + { processArguments.OutputDataReceived += OutputData; - - if (_onError != null) + } + + if (_onError != null || _onTimeProgress != null || (_onPercentageProgress != null && _totalTimespan != null)) + { processArguments.ErrorDataReceived += ErrorData; + } return processArguments; } @@ -216,22 +256,29 @@ private ProcessArguments PrepareProcessArguments(FFOptions ffOptions, private void ErrorData(object sender, string msg) { _onError?.Invoke(msg); + + var match = ProgressRegex.Match(msg); + if (!match.Success) + { + return; + } + + var processed = TimeSpan.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); + _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); - - var match = ProgressRegex.Match(msg); - if (!match.Success) return; - - var processed = TimeSpan.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); - _onTimeProgress?.Invoke(processed); - - if (_onPercentageProgress == null || _totalTimespan == null) return; - var percentage = Math.Round(processed.TotalSeconds / _totalTimespan.Value.TotalSeconds * 100, 2); - _onPercentageProgress(percentage); } } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/FFMpegArguments.cs b/FFMpegCore/FFMpeg/FFMpegArguments.cs index c100c85..1819cff 100644 --- a/FFMpegCore/FFMpeg/FFMpegArguments.cs +++ b/FFMpegCore/FFMpeg/FFMpegArguments.cs @@ -1,12 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -using FFMpegCore.Arguments; +using FFMpegCore.Arguments; using FFMpegCore.Builders.MetaData; using FFMpegCore.Pipes; @@ -14,7 +6,7 @@ namespace FFMpegCore { public sealed class FFMpegArguments : FFMpegArgumentsBase { - private readonly FFMpegGlobalArguments _globalArguments = new FFMpegGlobalArguments(); + private readonly FFMpegGlobalArguments _globalArguments = new(); private FFMpegArguments() { } @@ -34,7 +26,6 @@ private string GetText() 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); - public FFMpegArguments WithGlobalOptions(Action configureOptions) { configureOptions(_globalArguments); @@ -46,11 +37,11 @@ public FFMpegArguments WithGlobalOptions(Action configure public FFMpegArguments AddFileInput(string filePath, bool verifyExists = true, Action? addArguments = null) => WithInput(new InputArgument(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 /// @@ -83,7 +74,9 @@ private FFMpegArgumentProcessor ToProcessor(IOutputArgument argument, Action()) + { argument.Pre(); + } } internal async Task During(CancellationToken cancellationToken = default) { @@ -93,7 +86,9 @@ internal async Task During(CancellationToken cancellationToken = default) internal void Post() { foreach (var argument in Arguments.OfType()) + { argument.Post(); + } } } } diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentsBase.cs b/FFMpegCore/FFMpeg/FFMpegArgumentsBase.cs index fc51ab1..aae100a 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentsBase.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentsBase.cs @@ -1,10 +1,9 @@ -using System.Collections.Generic; -using FFMpegCore.Arguments; +using FFMpegCore.Arguments; namespace FFMpegCore { public abstract class FFMpegArgumentsBase { - internal readonly List Arguments = new List(); + internal readonly List Arguments = new(); } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/FFMpegCache.cs b/FFMpegCore/FFMpeg/FFMpegCache.cs index 0847202..a9a6c23 100644 --- a/FFMpegCore/FFMpeg/FFMpegCache.cs +++ b/FFMpegCore/FFMpeg/FFMpegCache.cs @@ -1,12 +1,10 @@ using FFMpegCore.Enums; -using System.Collections.Generic; -using System.Linq; namespace FFMpegCore { - static class FFMpegCache + internal static class FFMpegCache { - private static readonly object _syncObject = new object(); + private static readonly object _syncObject = new(); private static Dictionary? _pixelFormats; private static Dictionary? _codecs; private static Dictionary? _containers; @@ -16,39 +14,54 @@ public static IReadOnlyDictionary PixelFormats get { if (_pixelFormats == null) //First check not thread safe + { lock (_syncObject) + { if (_pixelFormats == null)//Second check thread safe + { _pixelFormats = FFMpeg.GetPixelFormatsInternal().ToDictionary(x => x.Name); + } + } + } return _pixelFormats; } - } public static IReadOnlyDictionary Codecs { get { if (_codecs == null) //First check not thread safe + { lock (_syncObject) + { if (_codecs == null)//Second check thread safe + { _codecs = FFMpeg.GetCodecsInternal(); + } + } + } return _codecs; } - } public static IReadOnlyDictionary ContainerFormats { get { if (_containers == null) //First check not thread safe + { lock (_syncObject) + { if (_containers == null)//Second check thread safe + { _containers = FFMpeg.GetContainersFormatsInternal().ToDictionary(x => x.Name); + } + } + } return _containers; } - } } } diff --git a/FFMpegCore/FFMpeg/FFMpegGlobalArguments.cs b/FFMpegCore/FFMpeg/FFMpegGlobalArguments.cs index e7d6e24..ebbb658 100644 --- a/FFMpegCore/FFMpeg/FFMpegGlobalArguments.cs +++ b/FFMpegCore/FFMpeg/FFMpegGlobalArguments.cs @@ -5,14 +5,13 @@ namespace FFMpegCore public sealed class FFMpegGlobalArguments : FFMpegArgumentsBase { internal FFMpegGlobalArguments() { } - + public FFMpegGlobalArguments WithVerbosityLevel(VerbosityLevel verbosityLevel = VerbosityLevel.Error) => WithOption(new VerbosityLevelArgument(verbosityLevel)); - + private FFMpegGlobalArguments WithOption(IArgument argument) { Arguments.Add(argument); return this; } - } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpeg/Pipes/IAudioSample.cs b/FFMpegCore/FFMpeg/Pipes/IAudioSample.cs index c7dea65..05f83ed 100644 --- a/FFMpegCore/FFMpeg/Pipes/IAudioSample.cs +++ b/FFMpegCore/FFMpeg/Pipes/IAudioSample.cs @@ -1,8 +1,4 @@ -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace FFMpegCore.Pipes +namespace FFMpegCore.Pipes { /// /// Interface for Audio sample diff --git a/FFMpegCore/FFMpeg/Pipes/IPipeSink.cs b/FFMpegCore/FFMpeg/Pipes/IPipeSink.cs index e5f2bf4..1e1e6c3 100644 --- a/FFMpegCore/FFMpeg/Pipes/IPipeSink.cs +++ b/FFMpegCore/FFMpeg/Pipes/IPipeSink.cs @@ -1,8 +1,4 @@ -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace FFMpegCore.Pipes +namespace FFMpegCore.Pipes { public interface IPipeSink { diff --git a/FFMpegCore/FFMpeg/Pipes/IPipeSource.cs b/FFMpegCore/FFMpeg/Pipes/IPipeSource.cs index c250421..33c0ab4 100644 --- a/FFMpegCore/FFMpeg/Pipes/IPipeSource.cs +++ b/FFMpegCore/FFMpeg/Pipes/IPipeSource.cs @@ -1,8 +1,4 @@ -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace FFMpegCore.Pipes +namespace FFMpegCore.Pipes { /// /// Interface for ffmpeg pipe source data IO diff --git a/FFMpegCore/FFMpeg/Pipes/IVideoFrame.cs b/FFMpegCore/FFMpeg/Pipes/IVideoFrame.cs index dd583d9..9254dad 100644 --- a/FFMpegCore/FFMpeg/Pipes/IVideoFrame.cs +++ b/FFMpegCore/FFMpeg/Pipes/IVideoFrame.cs @@ -1,8 +1,4 @@ -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace FFMpegCore.Pipes +namespace FFMpegCore.Pipes { /// /// Interface for Video frame diff --git a/FFMpegCore/FFMpeg/Pipes/PipeHelpers.cs b/FFMpegCore/FFMpeg/Pipes/PipeHelpers.cs index c680c3e..108c146 100644 --- a/FFMpegCore/FFMpeg/Pipes/PipeHelpers.cs +++ b/FFMpegCore/FFMpeg/Pipes/PipeHelpers.cs @@ -1,18 +1,19 @@ -using System; -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; namespace FFMpegCore.Pipes { - static class PipeHelpers + internal static class PipeHelpers { - public static string GetUnqiuePipeName() => $"FFMpegCore_{Guid.NewGuid()}"; + public static string GetUnqiuePipeName() => $"FFMpegCore_{Guid.NewGuid().ToString("N").Substring(0, 5)}"; public static string GetPipePath(string pipeName) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { return $@"\\.\pipe\{pipeName}"; - else - return $"unix:/tmp/CoreFxPipe_{pipeName}"; + } + + return $"unix:{Path.Combine(Path.GetTempPath(), $"CoreFxPipe_{pipeName}")}"; } } } diff --git a/FFMpegCore/FFMpeg/Pipes/RawAudioPipeSource.cs b/FFMpegCore/FFMpeg/Pipes/RawAudioPipeSource.cs index 8797694..653e252 100644 --- a/FFMpegCore/FFMpeg/Pipes/RawAudioPipeSource.cs +++ b/FFMpegCore/FFMpeg/Pipes/RawAudioPipeSource.cs @@ -1,9 +1,4 @@ -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace FFMpegCore.Pipes +namespace FFMpegCore.Pipes { /// /// Implementation of for a raw audio stream that is gathered from . diff --git a/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs b/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs index 0e3ab61..fe4c881 100644 --- a/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs +++ b/FFMpegCore/FFMpeg/Pipes/RawVideoPipeSource.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Threading; -using System.Threading.Tasks; +using System.Globalization; using FFMpegCore.Exceptions; namespace FFMpegCore.Pipes @@ -35,8 +30,11 @@ public string GetStreamArguments() if (_framesEnumerator.Current == null) { if (!_framesEnumerator.MoveNext()) + { throw new InvalidOperationException("Enumerator is empty, unable to get frame"); + } } + StreamFormat = _framesEnumerator.Current!.Format; Width = _framesEnumerator.Current!.Width; Height = _framesEnumerator.Current!.Height; @@ -65,9 +63,11 @@ public async Task WriteAsync(Stream outputStream, CancellationToken cancellation 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 289c0ea..33b5747 100644 --- a/FFMpegCore/FFMpeg/Pipes/StreamPipeSink.cs +++ b/FFMpegCore/FFMpeg/Pipes/StreamPipeSink.cs @@ -1,9 +1,4 @@ -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace FFMpegCore.Pipes +namespace FFMpegCore.Pipes { public class StreamPipeSink : IPipeSink { @@ -20,7 +15,7 @@ public StreamPipeSink(Stream destination) Writer = (inputStream, cancellationToken) => inputStream.CopyToAsync(destination, BlockSize, cancellationToken); } - public async Task ReadAsync(Stream inputStream, CancellationToken cancellationToken) + public async Task ReadAsync(Stream inputStream, CancellationToken cancellationToken) => await Writer(inputStream, cancellationToken).ConfigureAwait(false); public string GetFormat() => Format; diff --git a/FFMpegCore/FFMpeg/Pipes/StreamPipeSource.cs b/FFMpegCore/FFMpeg/Pipes/StreamPipeSource.cs index 99bc081..87f621f 100644 --- a/FFMpegCore/FFMpeg/Pipes/StreamPipeSource.cs +++ b/FFMpegCore/FFMpeg/Pipes/StreamPipeSource.cs @@ -1,8 +1,4 @@ -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace FFMpegCore.Pipes +namespace FFMpegCore.Pipes { /// /// Implementation of used for stream redirection diff --git a/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs b/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs new file mode 100644 index 0000000..4456837 --- /dev/null +++ b/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the MIT license. See License.txt in the project root for license information. + +using System.Drawing; +using FFMpegCore.Enums; + +namespace FFMpegCore; + +public static class SnapshotArgumentBuilder +{ + public static (FFMpegArguments, Action outputOptions) BuildSnapshotArguments( + string input, + IMediaAnalysis source, + Size? size = null, + TimeSpan? captureTime = null, + int? streamIndex = null, + int inputFileIndex = 0) + { + captureTime ??= TimeSpan.FromSeconds(source.Duration.TotalSeconds / 3); + size = PrepareSnapshotSize(source, size); + streamIndex ??= source.PrimaryVideoStream?.Index + ?? source.VideoStreams.FirstOrDefault()?.Index + ?? 0; + + return (FFMpegArguments + .FromFileInput(input, false, options => options + .Seek(captureTime)), + options => options + .SelectStream((int)streamIndex, inputFileIndex) + .WithVideoCodec(VideoCodec.Png) + .WithFrameOutputCount(1) + .Resize(size)); + } + + private static Size? PrepareSnapshotSize(IMediaAnalysis source, Size? wantedSize) + { + if (wantedSize == null || (wantedSize.Value.Height <= 0 && wantedSize.Value.Width <= 0) || source.PrimaryVideoStream == null) + { + return null; + } + + var currentSize = new Size(source.PrimaryVideoStream.Width, source.PrimaryVideoStream.Height); + if (source.PrimaryVideoStream.Rotation == 90 || source.PrimaryVideoStream.Rotation == 180) + { + currentSize = new Size(source.PrimaryVideoStream.Height, source.PrimaryVideoStream.Width); + } + + if (wantedSize.Value.Width != currentSize.Width || wantedSize.Value.Height != currentSize.Height) + { + if (wantedSize.Value.Width <= 0 && wantedSize.Value.Height > 0) + { + var ratio = (double)wantedSize.Value.Height / currentSize.Height; + return new Size((int)(currentSize.Width * ratio), (int)(currentSize.Height * ratio)); + } + + if (wantedSize.Value.Height <= 0 && wantedSize.Value.Width > 0) + { + var ratio = (double)wantedSize.Value.Width / currentSize.Width; + return new Size((int)(currentSize.Width * ratio), (int)(currentSize.Height * ratio)); + } + + return wantedSize; + } + + return null; + } +} diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index f40295c..7c3f7bb 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -1,45 +1,23 @@  - - en - https://github.com/rosenbjerg/FFMpegCore - https://github.com/rosenbjerg/FFMpegCore - - A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications - 4.0.0.0 - README.md - - Fixes for `MetaDataArgument` (thanks @JKamsker) -- Support for Audible `aaxc` (thanks @JKamsker) -- Include errordata in `IMediaAnalysis` (thanks @JKamsker) -- Pass `FFOptions` properly when using ffprobe (thanks @Notheisz57) -- CancellationToken support for `AnalyseAsync` -- Case-insensitive dictionaries for `Tags` and `Disposition` -- Fix for `PosterWithAudio` -- Fix for `JoinImageSequence` -- Updates to dependendies -- A lot of bug fixes - 8 - 4.8.0 - MIT - Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev - ffmpeg ffprobe convert video audio mediafile resize analyze muxing - GitHub - true - enable - netstandard2.0 - + + true + A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications + 5.0.0 + + + ffmpeg ffprobe convert video audio mediafile resize analyze muxing + Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev + README.md + - - - Always - - - + + + - - - - - + + + + diff --git a/FFMpegCore/FFOptions.cs b/FFMpegCore/FFOptions.cs index a34bca2..3194874 100644 --- a/FFMpegCore/FFOptions.cs +++ b/FFMpegCore/FFOptions.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; +using System.Text; +using FFMpegCore.Enums; namespace FFMpegCore { @@ -11,7 +9,7 @@ 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 /// @@ -21,16 +19,25 @@ public class FFOptions : ICloneable /// Folder used for temporary files necessary for static methods on FFMpeg class /// public string TemporaryFilesFolder { get; set; } = Path.GetTempPath(); - + /// /// Encoding used for parsing stdout/stderr on ffmpeg and ffprobe processes /// public Encoding Encoding { get; set; } = Encoding.Default; + /// + /// 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 Dictionary + public Dictionary ExtensionOverrides { get; set; } = new() { { "mpegts", ".ts" }, }; @@ -48,4 +55,4 @@ public class FFOptions : ICloneable /// public FFOptions Clone() => (FFOptions)MemberwiseClone(); } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFProbe/AudioStream.cs b/FFMpegCore/FFProbe/AudioStream.cs index 50c5572..871a78a 100644 --- a/FFMpegCore/FFProbe/AudioStream.cs +++ b/FFMpegCore/FFProbe/AudioStream.cs @@ -7,4 +7,4 @@ public class AudioStream : MediaStream public int SampleRateHz { get; set; } public string Profile { get; set; } = null!; } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFProbe/Exceptions/FFProbeException.cs b/FFMpegCore/FFProbe/Exceptions/FFProbeException.cs index 3495193..a17aec0 100644 --- a/FFMpegCore/FFProbe/Exceptions/FFProbeException.cs +++ b/FFMpegCore/FFProbe/Exceptions/FFProbeException.cs @@ -1,6 +1,4 @@ -using System; - -namespace FFMpegCore.Exceptions +namespace FFMpegCore.Exceptions { public class FFProbeException : Exception { @@ -8,4 +6,4 @@ public FFProbeException(string message, Exception? inner = null) : base(message, { } } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFProbe/Exceptions/FFProbeProcessException.cs b/FFMpegCore/FFProbe/Exceptions/FFProbeProcessException.cs index 5ab6b93..cdbeb55 100644 --- a/FFMpegCore/FFProbe/Exceptions/FFProbeProcessException.cs +++ b/FFMpegCore/FFProbe/Exceptions/FFProbeProcessException.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; - -namespace FFMpegCore.Exceptions +namespace FFMpegCore.Exceptions { public class FFProbeProcessException : FFProbeException { @@ -12,4 +9,4 @@ public FFProbeProcessException(string message, IReadOnlyCollection proce ProcessErrors = processErrors; } } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFProbe/Exceptions/FormatNullException.cs b/FFMpegCore/FFProbe/Exceptions/FormatNullException.cs index 4141f5f..04ae0a0 100644 --- a/FFMpegCore/FFProbe/Exceptions/FormatNullException.cs +++ b/FFMpegCore/FFProbe/Exceptions/FormatNullException.cs @@ -6,4 +6,4 @@ public FormatNullException() : base("Format not specified") { } } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFProbe/FFProbe.cs b/FFMpegCore/FFProbe/FFProbe.cs index 21cf3af..8a7069e 100644 --- a/FFMpegCore/FFProbe/FFProbe.cs +++ b/FFMpegCore/FFProbe/FFProbe.cs @@ -1,9 +1,5 @@ -using System; -using System.Diagnostics; -using System.IO; +using System.Diagnostics; using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; using FFMpegCore.Arguments; using FFMpegCore.Exceptions; using FFMpegCore.Helpers; @@ -17,11 +13,11 @@ public static class FFProbe public static IMediaAnalysis Analyse(string filePath, FFOptions? ffOptions = null) { ThrowIfInputFileDoesNotExist(filePath); - + var processArguments = PrepareStreamAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current); var result = processArguments.StartAndWaitForExit(); ThrowIfExitCodeNotZero(result); - + return ParseOutput(result); } @@ -72,16 +68,17 @@ public static IMediaAnalysis Analyse(Stream stream, FFOptions? ffOptions = null) { 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) { ThrowIfInputFileDoesNotExist(filePath); - + var instance = PrepareStreamAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current); var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); ThrowIfExitCodeNotZero(result); @@ -89,6 +86,15 @@ public static async Task AnalyseAsync(string filePath, FFOptions return ParseOutput(result); } + public static FFProbeFrames GetFrames(Uri uri, FFOptions? ffOptions = null) + { + var instance = PrepareFrameAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current); + var result = instance.StartAndWaitForExit(); + ThrowIfExitCodeNotZero(result); + + return ParseFramesOutput(result); + } + public static async Task GetFramesAsync(string filePath, FFOptions? ffOptions = null, CancellationToken cancellationToken = default) { ThrowIfInputFileDoesNotExist(filePath); @@ -127,20 +133,28 @@ public static async Task AnalyseAsync(Stream stream, FFOptions? { await pipeArgument.During(cancellationToken).ConfigureAwait(false); } - catch(IOException) + 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) + { + var instance = PrepareFrameAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current); + 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); @@ -148,10 +162,12 @@ private static IMediaAnalysis ParseOutput(IProcessResult instance) { PropertyNameCaseInsensitive = true }); - + if (ffprobeAnalysis?.Format == null) + { throw new FormatNullException(); - + } + ffprobeAnalysis.ErrorData = instance.ErrorData; return new MediaAnalysis(ffprobeAnalysis); } @@ -162,7 +178,7 @@ private static FFProbeFrames ParseFramesOutput(IProcessResult instance) { PropertyNameCaseInsensitive = true, NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString | System.Text.Json.Serialization.JsonNumberHandling.WriteAsString - }) ; + }); return ffprobeAnalysis!; } @@ -174,7 +190,7 @@ private static FFProbePackets ParsePacketsOutput(IProcessResult instance) { PropertyNameCaseInsensitive = true, NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString | System.Text.Json.Serialization.JsonNumberHandling.WriteAsString - }) ; + }); return ffprobeAnalysis!; } @@ -202,7 +218,7 @@ private static ProcessArguments PrepareFrameAnalysisInstance(string filePath, FF => PrepareInstance($"-loglevel error -print_format json -show_frames -v quiet -sexagesimal \"{filePath}\"", ffOptions); private static ProcessArguments PreparePacketAnalysisInstance(string filePath, FFOptions ffOptions) => PrepareInstance($"-loglevel error -print_format json -show_packets -v quiet -sexagesimal \"{filePath}\"", ffOptions); - + private static ProcessArguments PrepareInstance(string arguments, FFOptions ffOptions) { FFProbeHelper.RootExceptionCheck(); diff --git a/FFMpegCore/FFProbe/FFProbeAnalysis.cs b/FFMpegCore/FFProbe/FFProbeAnalysis.cs index cbbb9fd..b053d98 100644 --- a/FFMpegCore/FFProbe/FFProbeAnalysis.cs +++ b/FFMpegCore/FFProbe/FFProbeAnalysis.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; namespace FFMpegCore @@ -7,40 +7,43 @@ public class FFProbeAnalysis { [JsonPropertyName("streams")] public List Streams { get; set; } = null!; - + [JsonPropertyName("format")] public Format Format { get; set; } = null!; - + [JsonIgnore] - public IReadOnlyList ErrorData { get; set; } + 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!; @@ -53,6 +56,12 @@ public class FFProbeStream : ITagsContainer, IDispositionContainer [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!; @@ -67,10 +76,10 @@ public class FFProbeStream : ITagsContainer, IDispositionContainer [JsonPropertyName("r_frame_rate")] public string FrameRate { get; set; } = null!; - + [JsonPropertyName("pix_fmt")] public string PixelFormat { get; set; } = null!; - + [JsonPropertyName("sample_rate")] public string SampleRate { get; set; } = null!; @@ -79,6 +88,9 @@ public class FFProbeStream : ITagsContainer, IDispositionContainer [JsonPropertyName("tags")] public Dictionary Tags { get; set; } = null!; + + [JsonPropertyName("side_data_list")] + public List> SideData { get; set; } = null!; } public class Format : ITagsContainer @@ -108,7 +120,7 @@ public class Format : ITagsContainer public string Size { get; set; } = null!; [JsonPropertyName("bit_rate")] - public string BitRate { get; set; } = null!; + public string? BitRate { get; set; } = null!; [JsonPropertyName("probe_score")] public int ProbeScore { get; set; } @@ -132,10 +144,13 @@ public static class TagExtensions private static string? TryGetTagValue(ITagsContainer tagsContainer, string key) { if (tagsContainer.Tags != null && tagsContainer.Tags.TryGetValue(key, out var tagValue)) + { return tagValue; + } + return null; } - + 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"); @@ -147,7 +162,10 @@ 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; } diff --git a/FFMpegCore/FFProbe/FrameAnalysis.cs b/FFMpegCore/FFProbe/FrameAnalysis.cs index 08e5037..68ef500 100644 --- a/FFMpegCore/FFProbe/FrameAnalysis.cs +++ b/FFMpegCore/FFProbe/FrameAnalysis.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace FFMpegCore { @@ -7,70 +6,70 @@ public class FFProbeFrameAnalysis { [JsonPropertyName("media_type")] public string MediaType { get; set; } = null!; - + [JsonPropertyName("stream_index")] public int StreamIndex { get; set; } - + [JsonPropertyName("key_frame")] public int KeyFrame { get; set; } - + [JsonPropertyName("pkt_pts")] public long PacketPts { get; set; } - + [JsonPropertyName("pkt_pts_time")] public string PacketPtsTime { get; set; } = null!; - + [JsonPropertyName("pkt_dts")] public long PacketDts { get; set; } - + [JsonPropertyName("pkt_dts_time")] public string PacketDtsTime { get; set; } = null!; - + [JsonPropertyName("best_effort_timestamp")] public long BestEffortTimestamp { get; set; } - + [JsonPropertyName("best_effort_timestamp_time")] public string BestEffortTimestampTime { get; set; } = null!; - + [JsonPropertyName("pkt_duration")] public int PacketDuration { get; set; } - + [JsonPropertyName("pkt_duration_time")] public string PacketDurationTime { get; set; } = null!; - + [JsonPropertyName("pkt_pos")] public long PacketPos { get; set; } - + [JsonPropertyName("pkt_size")] public int PacketSize { get; set; } - + [JsonPropertyName("width")] public long Width { get; set; } - + [JsonPropertyName("height")] public long Height { get; set; } - + [JsonPropertyName("pix_fmt")] public string PixelFormat { get; set; } = null!; - + [JsonPropertyName("pict_type")] public string PictureType { get; set; } = null!; - + [JsonPropertyName("coded_picture_number")] public long CodedPictureNumber { get; set; } - + [JsonPropertyName("display_picture_number")] public long DisplayPictureNumber { get; set; } - + [JsonPropertyName("interlaced_frame")] public int InterlacedFrame { get; set; } - + [JsonPropertyName("top_field_first")] public int TopFieldFirst { get; set; } - + [JsonPropertyName("repeat_pict")] public int RepeatPicture { get; set; } - + [JsonPropertyName("chroma_location")] public string ChromaLocation { get; set; } = null!; } diff --git a/FFMpegCore/FFProbe/IMediaAnalysis.cs b/FFMpegCore/FFProbe/IMediaAnalysis.cs index 5884f74..47f47e4 100644 --- a/FFMpegCore/FFProbe/IMediaAnalysis.cs +++ b/FFMpegCore/FFProbe/IMediaAnalysis.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; - -namespace FFMpegCore +namespace FFMpegCore { public interface IMediaAnalysis { diff --git a/FFMpegCore/FFProbe/MediaAnalysis.cs b/FFMpegCore/FFProbe/MediaAnalysis.cs index e1fbd1d..53943dc 100644 --- a/FFMpegCore/FFProbe/MediaAnalysis.cs +++ b/FFMpegCore/FFProbe/MediaAnalysis.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text.RegularExpressions; +using System.Text.RegularExpressions; namespace FFMpegCore { @@ -13,14 +10,15 @@ internal MediaAnalysis(FFProbeAnalysis analysis) 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 ?? new List().AsReadOnly(); + 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, @@ -38,7 +36,7 @@ private MediaFormat ParseFormat(Format analysisFormat) }.Max(); public MediaFormat Format { 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(); @@ -47,7 +45,14 @@ private MediaFormat ParseFormat(Format analysisFormat) 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 : (int?)bitDepth; + } + private VideoStream ParseVideoStream(FFProbeStream stream) { return new VideoStream @@ -61,16 +66,19 @@ private VideoStream ParseVideoStream(FFProbeStream stream) CodecTag = stream.CodecTag, CodecTagString = stream.CodecTagString, DisplayAspectRatio = MediaAnalysisUtils.ParseRatioInt(stream.DisplayAspectRatio, ':'), - Duration = MediaAnalysisUtils.ParseDuration(stream), + 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, - Rotation = (int)float.Parse(stream.GetRotate() ?? "0"), + Rotation = MediaAnalysisUtils.ParseRotation(stream), Language = stream.GetLanguage(), Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition), Tags = stream.Tags.ToCaseInsensitive(), + BitDepth = GetBitDepth(stream), }; } @@ -86,12 +94,14 @@ private AudioStream ParseAudioStream(FFProbeStream stream) CodecTagString = stream.CodecTagString, Channels = stream.Channels ?? default, ChannelLayout = stream.ChannelLayout, - Duration = MediaAnalysisUtils.ParseDuration(stream), + 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), }; } @@ -103,18 +113,18 @@ private SubtitleStream ParseSubtitleStream(FFProbeStream stream) BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseLongInvariant(stream.BitRate) : default, CodecName = stream.CodecName, CodecLongName = stream.CodecLongName, - Duration = MediaAnalysisUtils.ParseDuration(stream), + 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 Regex(@"^(\d+):(\d{1,2}):(\d{1,2})\.(\d{1,3})", RegexOptions.Compiled); + 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) { @@ -124,14 +134,22 @@ public static class MediaAnalysisUtils public static (int, int) ParseRatioInt(string input, char separator) { - if (string.IsNullOrEmpty(input)) return (0, 0); + if (string.IsNullOrEmpty(input)) + { + return (0, 0); + } + var ratio = input.Split(separator); return (ParseIntInvariant(ratio[0]), ParseIntInvariant(ratio[1])); } public static (double, double) ParseRatioDouble(string input, char separator) { - if (string.IsNullOrEmpty(input)) return (0, 0); + if (string.IsNullOrEmpty(input)) + { + return (0, 0); + } + var ratio = input.Split(separator); return (ratio.Length > 0 ? ParseDoubleInvariant(ratio[0]) : 0, ratio.Length > 1 ? ParseDoubleInvariant(ratio[1]) : 0); } @@ -141,11 +159,10 @@ public static double ParseDoubleInvariant(string line) => public static int ParseIntInvariant(string line) => int.Parse(line, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture); - + public static long ParseLongInvariant(string line) => long.Parse(line, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture); - - + public static TimeSpan ParseDuration(string duration) { if (!string.IsNullOrEmpty(duration)) @@ -183,6 +200,20 @@ public static TimeSpan ParseDuration(FFProbeStream ffProbeStream) return ParseDuration(ffProbeStream.Duration); } + 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()); + } + else + { + return (int)float.Parse(fFProbeStream.GetRotate() ?? "0"); + } + } + public static Dictionary? FormatDisposition(Dictionary? disposition) { if (disposition == null) @@ -208,4 +239,4 @@ public static TimeSpan ParseDuration(FFProbeStream ffProbeStream) return result; } } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFProbe/MediaFormat.cs b/FFMpegCore/FFProbe/MediaFormat.cs index 874317c..c588165 100644 --- a/FFMpegCore/FFProbe/MediaFormat.cs +++ b/FFMpegCore/FFProbe/MediaFormat.cs @@ -1,11 +1,9 @@ -using System; -using System.Collections.Generic; - -namespace FFMpegCore +namespace FFMpegCore { public class MediaFormat { 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; } @@ -13,4 +11,4 @@ public class MediaFormat public double BitRate { get; set; } public Dictionary? Tags { get; set; } } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFProbe/MediaStream.cs b/FFMpegCore/FFProbe/MediaStream.cs index ffab04b..008390e 100644 --- a/FFMpegCore/FFProbe/MediaStream.cs +++ b/FFMpegCore/FFProbe/MediaStream.cs @@ -1,8 +1,5 @@ using FFMpegCore.Enums; -using System; -using System.Collections.Generic; - namespace FFMpegCore { public abstract class MediaStream @@ -13,11 +10,13 @@ public abstract class MediaStream 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; } + public Codec GetCodecInfo() => FFMpeg.GetCodec(CodecName); } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFProbe/PacketAnalysis.cs b/FFMpegCore/FFProbe/PacketAnalysis.cs index babe403..38ecd13 100644 --- a/FFMpegCore/FFProbe/PacketAnalysis.cs +++ b/FFMpegCore/FFProbe/PacketAnalysis.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; namespace FFMpegCore { @@ -13,19 +12,19 @@ public class FFProbePacketAnalysis [JsonPropertyName("pts")] public long Pts { get; set; } - + [JsonPropertyName("pts_time")] public string PtsTime { get; set; } = null!; [JsonPropertyName("dts")] public long Dts { get; set; } - + [JsonPropertyName("dts_time")] public string DtsTime { get; set; } = null!; [JsonPropertyName("duration")] public int Duration { get; set; } - + [JsonPropertyName("duration_time")] public string DurationTime { get; set; } = null!; diff --git a/FFMpegCore/FFProbe/ProcessArgumentsExtensions.cs b/FFMpegCore/FFProbe/ProcessArgumentsExtensions.cs index 1647e9b..47da20d 100644 --- a/FFMpegCore/FFProbe/ProcessArgumentsExtensions.cs +++ b/FFMpegCore/FFProbe/ProcessArgumentsExtensions.cs @@ -1,6 +1,4 @@ -using System.Threading; -using System.Threading.Tasks; -using Instances; +using Instances; namespace FFMpegCore { @@ -17,4 +15,4 @@ public static async Task StartAndWaitForExitAsync(this ProcessAr return await instance.WaitForExitAsync(cancellationToken); } } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFProbe/SubtitleStream.cs b/FFMpegCore/FFProbe/SubtitleStream.cs index 80493f4..83f02c8 100644 --- a/FFMpegCore/FFProbe/SubtitleStream.cs +++ b/FFMpegCore/FFProbe/SubtitleStream.cs @@ -4,4 +4,4 @@ public class SubtitleStream : MediaStream { } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFProbe/VideoStream.cs b/FFMpegCore/FFProbe/VideoStream.cs index a07cdd9..b41bb62 100644 --- a/FFMpegCore/FFProbe/VideoStream.cs +++ b/FFMpegCore/FFProbe/VideoStream.cs @@ -7,6 +7,7 @@ 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; } @@ -17,4 +18,4 @@ public class VideoStream : MediaStream public PixelFormat GetPixelFormatInfo() => FFMpeg.GetPixelFormat(PixelFormat); } -} \ No newline at end of file +} diff --git a/FFMpegCore/GlobalFFOptions.cs b/FFMpegCore/GlobalFFOptions.cs index 37340cc..209e137 100644 --- a/FFMpegCore/GlobalFFOptions.cs +++ b/FFMpegCore/GlobalFFOptions.cs @@ -1,29 +1,21 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; using System.Text.Json; namespace FFMpegCore { public static class GlobalFFOptions { - private static readonly string ConfigFile = "ffmpeg.config.json"; + private const string ConfigFile = "ffmpeg.config.json"; private static FFOptions? _current; - public static FFOptions Current - { - get { return _current ??= LoadFFOptions(); } - } + public static FFOptions Current => _current ??= LoadFFOptions(); + + public static void Configure(Action optionsAction) => optionsAction.Invoke(Current); - public static void Configure(Action optionsAction) - { - optionsAction?.Invoke(Current); - } public static void Configure(FFOptions ffOptions) { _current = ffOptions ?? throw new ArgumentNullException(nameof(ffOptions)); } - public static string GetFFMpegBinaryPath(FFOptions? ffOptions = null) => GetFFBinaryPath("FFMpeg", ffOptions ?? Current); @@ -33,25 +25,24 @@ private static string GetFFBinaryPath(string name, FFOptions ffOptions) { var ffName = name.ToLowerInvariant(); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { ffName += ".exe"; + } var target = Environment.Is64BitProcess ? "x64" : "x86"; if (Directory.Exists(Path.Combine(ffOptions.BinaryFolder, target))) + { ffName = Path.Combine(target, ffName); + } return Path.Combine(ffOptions.BinaryFolder, ffName); } private static FFOptions LoadFFOptions() { - if (File.Exists(ConfigFile)) - { - return JsonSerializer.Deserialize(File.ReadAllText(ConfigFile))!; - } - else - { - return new FFOptions(); - } + return File.Exists(ConfigFile) + ? JsonSerializer.Deserialize(File.ReadAllText(ConfigFile))! + : new FFOptions(); } } } diff --git a/FFMpegCore/Helpers/FFMpegHelper.cs b/FFMpegCore/Helpers/FFMpegHelper.cs index cb3b4cf..3fb03eb 100644 --- a/FFMpegCore/Helpers/FFMpegHelper.cs +++ b/FFMpegCore/Helpers/FFMpegHelper.cs @@ -1,7 +1,4 @@ -using System; -using System.Drawing; -using System.IO; -using FFMpegCore.Exceptions; +using FFMpegCore.Exceptions; using Instances; namespace FFMpegCore.Helpers @@ -10,38 +7,47 @@ public static class FFMpegHelper { private static bool _ffmpegVerified; - public static void ConversionSizeExceptionCheck(Image image) - => ConversionSizeExceptionCheck(image.Size.Width, image.Size.Height); - public static void ConversionSizeExceptionCheck(IMediaAnalysis info) => ConversionSizeExceptionCheck(info.PrimaryVideoStream!.Width, info.PrimaryVideoStream.Height); - private 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!"); + } } 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; + if (_ffmpegVerified) + { + return; + } + var result = Instance.Finish(GlobalFFOptions.GetFFMpegBinaryPath(ffMpegOptions), "-version"); _ffmpegVerified = result.ExitCode == 0; - if (!_ffmpegVerified) + if (!_ffmpegVerified) + { 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 f5b3472..0c44ab6 100644 --- a/FFMpegCore/Helpers/FFProbeHelper.cs +++ b/FFMpegCore/Helpers/FFProbeHelper.cs @@ -12,25 +12,39 @@ public static int Gcd(int first, int second) while (first != 0 && second != 0) { if (first > second) + { first -= second; - else second -= first; + } + else + { + second -= first; + } } + return first == 0 ? second : first; } public static void RootExceptionCheck() { if (GlobalFFOptions.Current.BinaryFolder == null) + { throw new FFOptionsException("FFProbe root is not configured in app config. Missing key 'BinaryFolder'."); + } } - + public static void VerifyFFProbeExists(FFOptions ffMpegOptions) { - if (_ffprobeVerified) return; + if (_ffprobeVerified) + { + return; + } + var result = Instance.Finish(GlobalFFOptions.GetFFProbeBinaryPath(ffMpegOptions), "-version"); _ffprobeVerified = result.ExitCode == 0; - if (!_ffprobeVerified) + if (!_ffprobeVerified) + { throw new FFProbeException("ffprobe was not found on your system"); + } } } } diff --git a/FFMpegCore/ImageInfo.cs b/FFMpegCore/ImageInfo.cs deleted file mode 100644 index cf8561e..0000000 --- a/FFMpegCore/ImageInfo.cs +++ /dev/null @@ -1,179 +0,0 @@ -using System; -using System.Drawing; -using System.IO; -using FFMpegCore.Enums; -using FFMpegCore.Helpers; - -namespace FFMpegCore -{ - public class ImageInfo - { - private FileInfo _file; - - /// - /// Create a image information object from a target path. - /// - /// Image file information. - public ImageInfo(FileInfo fileInfo) - { - if (!fileInfo.Extension.ToLowerInvariant().EndsWith(FileExtension.Png)) - { - throw new Exception("Image joining currently suppors only .png file types"); - } - - fileInfo.Refresh(); - - Size = fileInfo.Length / (1024 * 1024); - - using (var image = Image.FromFile(fileInfo.FullName)) - { - Width = image.Width; - Height = image.Height; - var cd = FFProbeHelper.Gcd(Width, Height); - Ratio = $"{Width / cd}:{Height / cd}"; - } - - - if (!fileInfo.Exists) - throw new ArgumentException($"Input file {fileInfo.FullName} does not exist!"); - - _file = fileInfo; - - - } - - /// - /// Create a image information object from a target path. - /// - /// Path to image. - public ImageInfo(string path) : this(new FileInfo(path)) { } - - /// - /// Aspect ratio. - /// - public string Ratio { get; internal set; } - - /// - /// Height of the image file. - /// - public int Height { get; internal set; } - - /// - /// Width of the image file. - /// - public int Width { get; internal set; } - - /// - /// Image file size in MegaBytes (MB). - /// - public double Size { get; internal set; } - - /// - /// Gets the name of the file. - /// - public string Name => _file.Name; - - /// - /// Gets the full path of the file. - /// - public string FullName => _file.FullName; - - /// - /// Gets the file extension. - /// - public string Extension => _file.Extension; - - /// - /// Gets a flag indicating if the file is read-only. - /// - public bool IsReadOnly => _file.IsReadOnly; - - /// - /// Gets a flag indicating if the file exists (no cache, per call verification). - /// - public bool Exists => File.Exists(FullName); - - /// - /// Gets the creation date. - /// - public DateTime CreationTime => _file.CreationTime; - - /// - /// Gets the parent directory information. - /// - public DirectoryInfo Directory => _file.Directory; - - /// - /// Create a image information object from a file information object. - /// - /// Image file information. - /// - public static ImageInfo FromFileInfo(FileInfo fileInfo) - { - return FromPath(fileInfo.FullName); - } - - /// - /// Create a image information object from a target path. - /// - /// Path to image. - /// - public static ImageInfo FromPath(string path) - { - return new ImageInfo(path); - } - - /// - /// Pretty prints the image information. - /// - /// - public override string ToString() - { - return "Image Path : " + FullName + Environment.NewLine + - "Image Root : " + Directory.FullName + Environment.NewLine + - "Image Name: " + Name + Environment.NewLine + - "Image Extension : " + Extension + Environment.NewLine + - "Aspect Ratio : " + Ratio + Environment.NewLine + - "Resolution : " + Width + "x" + Height + Environment.NewLine + - "Size : " + Size + " MB"; - } - - /// - /// Open a file stream. - /// - /// Opens a file in a specified mode. - /// File stream of the image file. - public FileStream FileOpen(FileMode mode) - { - return _file.Open(mode); - } - - /// - /// Move file to a specific directory. - /// - /// - public void MoveTo(DirectoryInfo destination) - { - var newLocation = $"{destination.FullName}{Path.DirectorySeparatorChar}{Name}{Extension}"; - _file.MoveTo(newLocation); - _file = new FileInfo(newLocation); - } - - /// - /// Delete the file. - /// - public void Delete() - { - _file.Delete(); - } - - /// - /// Converts image info to file info. - /// - /// A new FileInfo instance. - public FileInfo ToFileInfo() - { - return new FileInfo(_file.FullName); - } - } -} diff --git a/LICENSE b/LICENSE index cf79044..f389745 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2019 Vlad Jerca +Copyright (c) 2023 Vlad Jerca Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 7ed60ae..990361e 100644 --- a/README.md +++ b/README.md @@ -231,6 +231,6 @@ Older versions of ffmpeg might not support all ffmpeg arguments available throug ### License -Copyright © 2021 +Copyright © 2023 Released under [MIT license](https://github.com/rosenbjerg/FFMpegCore/blob/master/LICENSE)