diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 94a858ff..3f3677ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,42 +3,48 @@ name: CI on: push: branches: - - master + - main paths: - .github/workflows/ci.yml + - Directory.Build.props - FFMpegCore/** - FFMpegCore.Test/** + - FFMpegCore.Extensions.SkiaSharp/** + - FFMpegCore.Extensions.System.Drawing.Common/** pull_request: branches: - main - release paths: - - .github/workflows/ci.yml - - FFMpegCore/** - - FFMpegCore.Test/** + - .github/workflows/ci.yml + - Directory.Build.props + - FFMpegCore/** + - FFMpegCore.Test/** + - FFMpegCore.Extensions.SkiaSharp/** + - FFMpegCore.Extensions.System.Drawing.Common/** jobs: ci: runs-on: ${{ matrix.os }} strategy: matrix: - os: [windows-latest, ubuntu-latest, macos-latest] + os: [windows-latest, ubuntu-latest, macos-13] timeout-minutes: 7 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Prepare .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: '7.0.x' + dotnet-version: '8.0.x' - name: Lint with dotnet run: dotnet format FFMpegCore.sln --severity warn --verify-no-changes - name: Prepare FFMpeg - uses: FedericoCarboni/setup-ffmpeg@v2 + uses: FedericoCarboni/setup-ffmpeg@v3 with: token: ${{ secrets.GITHUB_TOKEN }} @@ -47,7 +53,9 @@ jobs: - if: matrix.os == 'windows-latest' name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: + fail_ci_if_error: true directory: FFMpegCore.Test/TestResults - fail_ci_if_error: true \ No newline at end of file + token: ${{ secrets.CODECOV_TOKEN }} + os: windows diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 00a1ea7d..f832def4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,12 +8,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Prepare .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: '7.0.x' + dotnet-version: '8.0.x' - name: Build solution run: dotnet pack FFMpegCore.sln -c Release diff --git a/Directory.Build.props b/Directory.Build.props index 2944c6bf..628195a6 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -13,5 +13,14 @@ https://github.com/rosenbjerg/FFMpegCore MIT en + + true + true + snupkg + true + + + true + \ No newline at end of file diff --git a/FFMpegCore.Examples/FFMpegCore.Examples.csproj b/FFMpegCore.Examples/FFMpegCore.Examples.csproj index f4a2b9bb..1aa7d463 100644 --- a/FFMpegCore.Examples/FFMpegCore.Examples.csproj +++ b/FFMpegCore.Examples/FFMpegCore.Examples.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net8.0 false diff --git a/FFMpegCore.Extensions.SkiaSharp/FFMpegCore.Extensions.SkiaSharp.csproj b/FFMpegCore.Extensions.SkiaSharp/FFMpegCore.Extensions.SkiaSharp.csproj index 87710cc1..9867a9ca 100644 --- a/FFMpegCore.Extensions.SkiaSharp/FFMpegCore.Extensions.SkiaSharp.csproj +++ b/FFMpegCore.Extensions.SkiaSharp/FFMpegCore.Extensions.SkiaSharp.csproj @@ -3,17 +3,18 @@ true Image extension for FFMpegCore using SkiaSharp - 5.0.0 + 5.0.1 ../nupkg ffmpeg ffprobe convert video audio mediafile resize analyze muxing skiasharp Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev, Dimitri Vranken + true - - + + diff --git a/FFMpegCore.Extensions.System.Drawing.Common/FFMpegCore.Extensions.System.Drawing.Common.csproj b/FFMpegCore.Extensions.System.Drawing.Common/FFMpegCore.Extensions.System.Drawing.Common.csproj index 13cdc1ab..885779a8 100644 --- a/FFMpegCore.Extensions.System.Drawing.Common/FFMpegCore.Extensions.System.Drawing.Common.csproj +++ b/FFMpegCore.Extensions.System.Drawing.Common/FFMpegCore.Extensions.System.Drawing.Common.csproj @@ -3,16 +3,17 @@ true Image extension for FFMpegCore using System.Common.Drawing - 5.0.0 + 5.0.1 ../nupkg ffmpeg ffprobe convert video audio mediafile resize analyze muxing Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev + true - + diff --git a/FFMpegCore.Test/ArgumentBuilderTest.cs b/FFMpegCore.Test/ArgumentBuilderTest.cs index cf455c82..ce39c9d8 100644 --- a/FFMpegCore.Test/ArgumentBuilderTest.cs +++ b/FFMpegCore.Test/ArgumentBuilderTest.cs @@ -9,6 +9,7 @@ namespace FFMpegCore.Test public class ArgumentBuilderTest { private readonly string[] _concatFiles = { "1.mp4", "2.mp4", "3.mp4", "4.mp4" }; + private readonly string[] _multiFiles = { "1.mp3", "2.mp3", "3.mp3", "4.mp3" }; [TestMethod] public void Builder_BuildString_IO_1() @@ -611,5 +612,86 @@ public void Builder_BuildString_TeeOutput() -i "input.mp4" -f tee "[movflags=faststart]output.mp4|[f=mpegts:select=\'0:v:0\']http://server/path" """, str); } + [TestMethod] + public void Builder_BuildString_MultiInput() + { + var audioStreams = string.Join("", _multiFiles.Select((item, index) => $"[{index}:0]")); + var mixFilter = $"{audioStreams}amix=inputs={_multiFiles.Length}:duration=longest:dropout_transition=1:normalize=0[final]"; + var ffmpegArgs = $"-filter_complex \"{mixFilter}\" -map \"[final]\""; + var str = FFMpegArguments + .FromFileInput(_multiFiles) + .OutputToFile("output.mp3", overwrite: true, options => options + .WithCustomArgument(ffmpegArgs) + .WithAudioCodec(AudioCodec.LibMp3Lame) // Set the audio codec to MP3 + .WithAudioBitrate(128) // Set the bitrate to 128kbps + .WithAudioSamplingRate(48000) // Set the sample rate to 48kHz + .WithoutMetadata() // Remove metadata + .WithCustomArgument("-ac 2 -write_xing 0 -id3v2_version 0")) // Force 2 Channels + .Arguments; + Assert.AreEqual($"-i \"1.mp3\" -i \"2.mp3\" -i \"3.mp3\" -i \"4.mp3\" -filter_complex \"[0:0][1:0][2:0][3:0]amix=inputs=4:duration=longest:dropout_transition=1:normalize=0[final]\" -map \"[final]\" -c:a libmp3lame -b:a 128k -ar 48000 -map_metadata -1 -ac 2 -write_xing 0 -id3v2_version 0 \"output.mp3\" -y", str); + } + [TestMethod] + public void Pre_VerifyExists_AllFilesExist() + { + // Arrange + var filePaths = new List + { + Path.GetTempFileName(), + Path.GetTempFileName(), + Path.GetTempFileName() + }; + var argument = new MultiInputArgument(true, filePaths); + try + { + // Act & Assert + argument.Pre(); // No exception should be thrown + } + finally + { + // Cleanup + foreach (var filePath in filePaths) + { + File.Delete(filePath); + } + } + } + + [TestMethod] + public void Pre_VerifyExists_SomeFilesNotExist() + { + // Arrange + var filePaths = new List + { + Path.GetTempFileName(), + "file2.mp4", + "file3.mp4" + }; + var argument = new MultiInputArgument(true, filePaths); + try + { + // Act & Assert + Assert.ThrowsException(() => argument.Pre()); + } + finally + { + // Cleanup + File.Delete(filePaths[0]); + } + } + + [TestMethod] + public void Pre_VerifyExists_NoFilesExist() + { + // Arrange + var filePaths = new List + { + "file1.mp4", + "file2.mp4", + "file3.mp4" + }; + var argument = new MultiInputArgument(true, filePaths); + // Act & Assert + Assert.ThrowsException(() => argument.Pre()); + } } } diff --git a/FFMpegCore.Test/FFMpegCore.Test.csproj b/FFMpegCore.Test/FFMpegCore.Test.csproj index 7c4b8884..77914234 100644 --- a/FFMpegCore.Test/FFMpegCore.Test.csproj +++ b/FFMpegCore.Test/FFMpegCore.Test.csproj @@ -1,26 +1,26 @@  - net6.0 + net8.0 false disable default - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + @@ -51,9 +51,15 @@ PreserveNewest + + PreserveNewest + PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/FFMpegCore.Test/FFProbeTests.cs b/FFMpegCore.Test/FFProbeTests.cs index a5070785..6ebad0f3 100644 --- a/FFMpegCore.Test/FFProbeTests.cs +++ b/FFMpegCore.Test/FFProbeTests.cs @@ -123,6 +123,7 @@ public void Probe_Success() Assert.AreEqual(1, info.PrimaryVideoStream.SampleAspectRatio.Width); Assert.AreEqual(1, info.PrimaryVideoStream.SampleAspectRatio.Height); Assert.AreEqual("yuv420p", info.PrimaryVideoStream.PixelFormat); + Assert.AreEqual(31, info.PrimaryVideoStream.Level); Assert.AreEqual(1280, info.PrimaryVideoStream.Width); Assert.AreEqual(720, info.PrimaryVideoStream.Height); Assert.AreEqual(25, info.PrimaryVideoStream.AvgFrameRate); @@ -145,6 +146,13 @@ public void Probe_Rotation() Assert.AreEqual(90, info.PrimaryVideoStream.Rotation); } + [TestMethod] + public void Probe_Rotation_Negative_Value() + { + var info = FFProbe.Analyse(TestResources.Mp4VideoRotationNegative); + Assert.AreEqual(-90, info.PrimaryVideoStream.Rotation); + } + [TestMethod, Timeout(10000)] public async Task Probe_Async_Success() { @@ -173,6 +181,18 @@ public async Task Probe_Success_FromStream_Async() Assert.AreEqual(3, info.Duration.Seconds); } + [TestMethod, Timeout(10000)] + public void Probe_HDR() + { + var info = FFProbe.Analyse(TestResources.HdrVideo); + + Assert.IsNotNull(info.PrimaryVideoStream); + Assert.AreEqual("tv", info.PrimaryVideoStream.ColorRange); + Assert.AreEqual("bt2020nc", info.PrimaryVideoStream.ColorSpace); + Assert.AreEqual("arib-std-b67", info.PrimaryVideoStream.ColorTransfer); + Assert.AreEqual("bt2020", info.PrimaryVideoStream.ColorPrimaries); + } + [TestMethod, Timeout(10000)] public async Task Probe_Success_Subtitle_Async() { diff --git a/FFMpegCore.Test/Resources/TestResources.cs b/FFMpegCore.Test/Resources/TestResources.cs index b958b80b..97e78924 100644 --- a/FFMpegCore.Test/Resources/TestResources.cs +++ b/FFMpegCore.Test/Resources/TestResources.cs @@ -4,7 +4,9 @@ public static class TestResources { public static readonly string Mp4Video = "./Resources/input_3sec.mp4"; public static readonly string Mp4VideoRotation = "./Resources/input_3sec_rotation_90deg.mp4"; + public static readonly string Mp4VideoRotationNegative = "./Resources/input_3sec_rotation_negative_90deg.mp4"; public static readonly string WebmVideo = "./Resources/input_3sec.webm"; + public static readonly string HdrVideo = "./Resources/input_hdr.mov"; public static readonly string Mp4WithoutVideo = "./Resources/input_audio_only_10sec.mp4"; public static readonly string Mp4WithoutAudio = "./Resources/input_video_only_3sec.mp4"; public static readonly string RawAudio = "./Resources/audio.raw"; diff --git a/FFMpegCore.Test/Resources/input_3sec_rotation_negative_90deg.mp4 b/FFMpegCore.Test/Resources/input_3sec_rotation_negative_90deg.mp4 new file mode 100644 index 00000000..9a135f08 Binary files /dev/null and b/FFMpegCore.Test/Resources/input_3sec_rotation_negative_90deg.mp4 differ diff --git a/FFMpegCore.Test/Resources/input_hdr.mov b/FFMpegCore.Test/Resources/input_hdr.mov new file mode 100644 index 00000000..3fdeea11 Binary files /dev/null and b/FFMpegCore.Test/Resources/input_hdr.mov differ diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 5071a483..8da9c19a 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -480,6 +480,21 @@ public void Video_Snapshot_PersistSnapshot() Assert.AreEqual("png", analysis.PrimaryVideoStream!.CodecName); } + [TestMethod, Timeout(BaseTimeoutMilliseconds)] + public void Video_Snapshot_Rotated_PersistSnapshot() + { + using var outputPath = new TemporaryFile("out.png"); + + var size = new Size(360, 0); // half the size of original video, keeping height 0 for keeping aspect ratio + FFMpeg.Snapshot(TestResources.Mp4VideoRotationNegative, outputPath, size); + + var analysis = FFProbe.Analyse(outputPath); + Assert.AreEqual(size.Width, analysis.PrimaryVideoStream!.Width); + Assert.AreEqual(1280 / 2, analysis.PrimaryVideoStream!.Height); + Assert.AreEqual(0, analysis.PrimaryVideoStream!.Rotation); + Assert.AreEqual("png", analysis.PrimaryVideoStream!.CodecName); + } + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public void Video_GifSnapshot_PersistSnapshot() { diff --git a/FFMpegCore/FFMpeg/Arguments/CropArgument.cs b/FFMpegCore/FFMpeg/Arguments/CropArgument.cs new file mode 100644 index 00000000..4fe10d64 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/CropArgument.cs @@ -0,0 +1,22 @@ +using System.Drawing; + +namespace FFMpegCore.Arguments +{ + public class CropArgument : IArgument + { + public readonly Size? Size; + public readonly int Top; + public readonly int Left; + + public CropArgument(Size? size, int top, int left) + { + Size = size; + Top = top; + Left = left; + } + + public CropArgument(int width, int height, int top, int left) : this(new Size(width, height), top, left) { } + + public string Text => Size == null ? string.Empty : $"-vf crop={Size.Value.Width}:{Size.Value.Height}:{Left}:{Top}"; + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/MultiInputArgument.cs b/FFMpegCore/FFMpeg/Arguments/MultiInputArgument.cs new file mode 100644 index 00000000..288c7615 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/MultiInputArgument.cs @@ -0,0 +1,47 @@ +namespace FFMpegCore.Arguments +{ + /// + /// Represents input parameters for multiple files + /// + public class MultiInputArgument : IInputArgument + { + public readonly bool VerifyExists; + public readonly IEnumerable FilePaths; + + public MultiInputArgument(bool verifyExists, IEnumerable filePaths) + { + VerifyExists = verifyExists; + FilePaths = filePaths; + } + + public MultiInputArgument(IEnumerable filePaths, bool verifyExists) : this(verifyExists, filePaths) { } + + public void Pre() + { + if (VerifyExists) + { + var missingFiles = new List(); + foreach (var filePath in FilePaths) + { + if (!File.Exists(filePath)) + { + missingFiles.Add(filePath); + } + } + + if (missingFiles.Any()) + { + throw new FileNotFoundException($"The following input files were not found: {string.Join(", ", missingFiles)}"); + } + } + } + + public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask; + public void Post() { } + + /// + /// Generates a combined input argument text for all file paths + /// + public string Text => string.Join(" ", FilePaths.Select(filePath => $"-i \"{filePath}\"")); + } +} diff --git a/FFMpegCore/FFMpeg/Enums/Enums.cs b/FFMpegCore/FFMpeg/Enums/Enums.cs index 4974b448..1f002037 100644 --- a/FFMpegCore/FFMpeg/Enums/Enums.cs +++ b/FFMpegCore/FFMpeg/Enums/Enums.cs @@ -17,6 +17,7 @@ public static class VideoCodec public static Codec LibTheora => FFMpeg.GetCodec("libtheora"); public static Codec Png => FFMpeg.GetCodec("png"); public static Codec MpegTs => FFMpeg.GetCodec("mpegts"); + public static Codec LibaomAv1 => FFMpeg.GetCodec("libaom-av1"); } public static class AudioCodec @@ -27,6 +28,8 @@ public static class AudioCodec public static Codec Ac3 => FFMpeg.GetCodec("ac3"); public static Codec Eac3 => FFMpeg.GetCodec("eac3"); public static Codec LibMp3Lame => FFMpeg.GetCodec("libmp3lame"); + public static Codec Copy => new Codec("copy", CodecType.Audio); + } public static class VideoType diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs index 6a6586c4..941c1c27 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs @@ -16,7 +16,10 @@ internal FFMpegArgumentOptions() { } public FFMpegArgumentOptions WithVariableBitrate(int vbr) => WithArgument(new VariableBitRateArgument(vbr)); public FFMpegArgumentOptions Resize(int width, int height) => WithArgument(new SizeArgument(width, height)); public FFMpegArgumentOptions Resize(Size? size) => WithArgument(new SizeArgument(size)); - + public FFMpegArgumentOptions Crop(Size? size, int left, int top) => WithArgument(new CropArgument(size, top, left)); + public FFMpegArgumentOptions Crop(int width, int height, int left, int top) => WithArgument(new CropArgument(new Size(width, height), top, left)); + public FFMpegArgumentOptions Crop(Size? size) => WithArgument(new CropArgument(size, 0, 0)); + public FFMpegArgumentOptions Crop(int width, int height) => WithArgument(new CropArgument(new Size(width, height), 0, 0)); public FFMpegArgumentOptions WithBitStreamFilter(Channel channel, Filter filter) => WithArgument(new BitStreamFilterArgument(channel, filter)); public FFMpegArgumentOptions WithConstantRateFactor(int crf) => WithArgument(new ConstantRateFactorArgument(crf)); public FFMpegArgumentOptions CopyChannel(Channel channel = Channel.Both) => WithArgument(new CopyArgument(channel)); diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs index 27237d8c..3a587049 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs @@ -1,5 +1,4 @@ using System.Diagnostics; -using System.Globalization; using System.Text.RegularExpressions; using FFMpegCore.Enums; using FFMpegCore.Exceptions; @@ -263,7 +262,7 @@ private void ErrorData(object sender, string msg) return; } - var processed = TimeSpan.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); + var processed = MediaAnalysisUtils.ParseDuration(match.Groups[1].Value); _onTimeProgress?.Invoke(processed); if (_onPercentageProgress == null || _totalTimespan == null) diff --git a/FFMpegCore/FFMpeg/FFMpegArguments.cs b/FFMpegCore/FFMpeg/FFMpegArguments.cs index cfc6d9de..ddb1f722 100644 --- a/FFMpegCore/FFMpeg/FFMpegArguments.cs +++ b/FFMpegCore/FFMpeg/FFMpegArguments.cs @@ -21,6 +21,7 @@ private string GetText() public static FFMpegArguments FromConcatInput(IEnumerable filePaths, Action? addArguments = null) => new FFMpegArguments().WithInput(new ConcatArgument(filePaths), addArguments); public static FFMpegArguments FromDemuxConcatInput(IEnumerable filePaths, Action? addArguments = null) => new FFMpegArguments().WithInput(new DemuxConcatArgument(filePaths), addArguments); public static FFMpegArguments FromFileInput(string filePath, bool verifyExists = true, Action? addArguments = null) => new FFMpegArguments().WithInput(new InputArgument(verifyExists, filePath), addArguments); + public static FFMpegArguments FromFileInput(IEnumerable filePath, bool verifyExists = true, Action? addArguments = null) => new FFMpegArguments().WithInput(new MultiInputArgument(verifyExists, filePath), addArguments); public static FFMpegArguments FromFileInput(FileInfo fileInfo, Action? addArguments = null) => new FFMpegArguments().WithInput(new InputArgument(fileInfo.FullName, false), addArguments); public static FFMpegArguments FromUrlInput(Uri uri, Action? addArguments = null) => new FFMpegArguments().WithInput(new InputArgument(uri.AbsoluteUri, false), addArguments); public static FFMpegArguments FromDeviceInput(string device, Action? addArguments = null) => new FFMpegArguments().WithInput(new InputDeviceArgument(device), addArguments); @@ -35,6 +36,7 @@ public FFMpegArguments WithGlobalOptions(Action configure public FFMpegArguments AddConcatInput(IEnumerable filePaths, Action? addArguments = null) => WithInput(new ConcatArgument(filePaths), addArguments); public FFMpegArguments AddDemuxConcatInput(IEnumerable filePaths, Action? addArguments = null) => WithInput(new DemuxConcatArgument(filePaths), addArguments); public FFMpegArguments AddFileInput(string filePath, bool verifyExists = true, Action? addArguments = null) => WithInput(new InputArgument(verifyExists, filePath), addArguments); + public FFMpegArguments AddFileInput(IEnumerable filePath, bool verifyExists = true, Action? addArguments = null) => WithInput(new MultiInputArgument(verifyExists, filePath), addArguments); public FFMpegArguments AddFileInput(FileInfo fileInfo, Action? addArguments = null) => WithInput(new InputArgument(fileInfo.FullName, false), addArguments); public FFMpegArguments AddUrlInput(Uri uri, Action? addArguments = null) => WithInput(new InputArgument(uri.AbsoluteUri, false), addArguments); public FFMpegArguments AddDeviceInput(string device, Action? addArguments = null) => WithInput(new InputDeviceArgument(device), addArguments); diff --git a/FFMpegCore/FFMpeg/Pipes/RawAudioPipeSource.cs b/FFMpegCore/FFMpeg/Pipes/RawAudioPipeSource.cs index 653e2520..cfe31ef3 100644 --- a/FFMpegCore/FFMpeg/Pipes/RawAudioPipeSource.cs +++ b/FFMpegCore/FFMpeg/Pipes/RawAudioPipeSource.cs @@ -27,7 +27,7 @@ public string GetStreamArguments() public async Task WriteAsync(Stream outputStream, CancellationToken cancellationToken) { - if (_sampleEnumerator.Current != null) + if (_sampleEnumerator.MoveNext() && _sampleEnumerator.Current != null) { await _sampleEnumerator.Current.SerializeAsync(outputStream, cancellationToken).ConfigureAwait(false); } diff --git a/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs b/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs index 0d9b414b..7d831834 100644 --- a/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs +++ b/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs @@ -64,7 +64,7 @@ public static (FFMpegArguments, Action outputOptions) Bui } var currentSize = new Size(source.PrimaryVideoStream.Width, source.PrimaryVideoStream.Height); - if (source.PrimaryVideoStream.Rotation == 90 || source.PrimaryVideoStream.Rotation == 180) + if (IsRotated(source.PrimaryVideoStream.Rotation)) { currentSize = new Size(source.PrimaryVideoStream.Height, source.PrimaryVideoStream.Width); } @@ -88,4 +88,10 @@ public static (FFMpegArguments, Action outputOptions) Bui return null; } + + private static bool IsRotated(int rotation) + { + var absRotation = Math.Abs(rotation); + return absRotation == 90 || absRotation == 180; + } } diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index ed3b71c8..2563fb5e 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -3,7 +3,7 @@ true A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications - 5.1.0 + 5.1.1 ../nupkg @@ -18,7 +18,9 @@ - + + + diff --git a/FFMpegCore/FFOptions.cs b/FFMpegCore/FFOptions.cs index 3194874f..2d4e4c98 100644 --- a/FFMpegCore/FFOptions.cs +++ b/FFMpegCore/FFOptions.cs @@ -1,4 +1,5 @@ using System.Text; +using System.Text.Json.Serialization; using FFMpegCore.Enums; namespace FFMpegCore @@ -20,10 +21,20 @@ public class FFOptions : ICloneable /// public string TemporaryFilesFolder { get; set; } = Path.GetTempPath(); + /// + /// Encoding web name used to persist encoding + /// + public string EncodingWebName { get; set; } = Encoding.Default.WebName; + /// /// Encoding used for parsing stdout/stderr on ffmpeg and ffprobe processes /// - public Encoding Encoding { get; set; } = Encoding.Default; + [JsonIgnore] + public Encoding Encoding + { + get => Encoding.GetEncoding(EncodingWebName); + set => EncodingWebName = value?.WebName ?? Encoding.Default.WebName; + } /// /// The log level to use when calling of the ffmpeg executable. diff --git a/FFMpegCore/FFProbe/FFProbeAnalysis.cs b/FFMpegCore/FFProbe/FFProbeAnalysis.cs index e88f4618..9f8e880c 100644 --- a/FFMpegCore/FFProbe/FFProbeAnalysis.cs +++ b/FFMpegCore/FFProbe/FFProbeAnalysis.cs @@ -83,6 +83,9 @@ public class FFProbeStream : ITagsContainer, IDispositionContainer [JsonPropertyName("pix_fmt")] public string PixelFormat { get; set; } = null!; + [JsonPropertyName("level")] + public int Level { get; set; } + [JsonPropertyName("sample_rate")] public string SampleRate { get; set; } = null!; @@ -94,6 +97,18 @@ public class FFProbeStream : ITagsContainer, IDispositionContainer [JsonPropertyName("side_data_list")] public List> SideData { get; set; } = null!; + + [JsonPropertyName("color_range")] + public string ColorRange { get; set; } = null!; + + [JsonPropertyName("color_space")] + public string ColorSpace { get; set; } = null!; + + [JsonPropertyName("color_transfer")] + public string ColorTransfer { get; set; } = null!; + + [JsonPropertyName("color_primaries")] + public string ColorPrimaries { get; set; } = null!; } public class Format : ITagsContainer diff --git a/FFMpegCore/FFProbe/MediaAnalysis.cs b/FFMpegCore/FFProbe/MediaAnalysis.cs index 887baeba..bf16af6b 100644 --- a/FFMpegCore/FFProbe/MediaAnalysis.cs +++ b/FFMpegCore/FFProbe/MediaAnalysis.cs @@ -30,9 +30,12 @@ private MediaFormat ParseFormat(Format analysisFormat) }; } + private string GetValue(string tagName, Dictionary? tags, string defaultValue) => + tags == null ? defaultValue : tags.TryGetValue(tagName, out var value) ? value : defaultValue; + private ChapterData ParseChapter(Chapter analysisChapter) { - var title = analysisChapter.Tags.FirstOrDefault(t => t.Key == "title").Value; + var title = GetValue("title", analysisChapter.Tags, "TitleValueNotSet"); var start = MediaAnalysisUtils.ParseDuration(analysisChapter.StartTime); var end = MediaAnalysisUtils.ParseDuration(analysisChapter.EndTime); @@ -87,6 +90,11 @@ private VideoStream ParseVideoStream(FFProbeStream stream) Width = stream.Width ?? 0, Profile = stream.Profile, PixelFormat = stream.PixelFormat, + Level = stream.Level, + ColorRange = stream.ColorRange, + ColorSpace = stream.ColorSpace, + ColorTransfer = stream.ColorTransfer, + ColorPrimaries = stream.ColorPrimaries, Rotation = MediaAnalysisUtils.ParseRotation(stream), Language = stream.GetLanguage(), Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition), diff --git a/FFMpegCore/FFProbe/VideoStream.cs b/FFMpegCore/FFProbe/VideoStream.cs index b41bb627..377b2d0c 100644 --- a/FFMpegCore/FFProbe/VideoStream.cs +++ b/FFMpegCore/FFProbe/VideoStream.cs @@ -13,8 +13,13 @@ public class VideoStream : MediaStream public int Height { get; set; } public double FrameRate { get; set; } public string PixelFormat { get; set; } = null!; + public int Level { get; set; } public int Rotation { get; set; } public double AverageFrameRate { get; set; } + public string ColorRange { get; set; } = null!; + public string ColorSpace { get; set; } = null!; + public string ColorTransfer { get; set; } = null!; + public string ColorPrimaries { get; set; } = null!; public PixelFormat GetPixelFormatInfo() => FFMpeg.GetPixelFormat(PixelFormat); }