diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 91fbab6..0da1b83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,28 +1,24 @@ name: CI on: - push: - branches: - - master - paths: - - .github/workflows/ci.yml - - FFMpegCore/** - - FFMpegCore.Test/** 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-13] + os: [windows-latest, ubuntu-latest, macos-latest] timeout-minutes: 7 steps: @@ -34,21 +30,24 @@ jobs: with: dotnet-version: '8.0.x' - - name: Lint with dotnet + - if: matrix.os == 'ubuntu-latest' + name: Lint with dotnet run: dotnet format FFMpegCore.sln --severity warn --verify-no-changes - - name: Prepare FFMpeg - uses: FedericoCarboni/setup-ffmpeg@v3 + - name: Setup FFmpeg + uses: AnimMouse/setup-ffmpeg@ae28d57dabbb148eff63170b6bf7f2b60062cbae # 1.1.0 with: - token: ${{ secrets.GITHUB_TOKEN }} + version: ${{ matrix.os != 'macos-latest' && '7.1' || '711' }} + token: ${{ github.token }} - name: Test with dotnet run: dotnet test FFMpegCore.sln --collect "XPlat Code Coverage" --logger GitHubActions - if: matrix.os == 'windows-latest' name: Upload coverage reports to Codecov - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v4 with: fail_ci_if_error: true directory: FFMpegCore.Test/TestResults token: ${{ secrets.CODECOV_TOKEN }} + os: windows diff --git a/Directory.Build.props b/Directory.Build.props index 2944c6b..628195a 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.Extensions.SkiaSharp/FFMpegCore.Extensions.SkiaSharp.csproj b/FFMpegCore.Extensions.SkiaSharp/FFMpegCore.Extensions.SkiaSharp.csproj index 3b16e06..3609ddf 100644 --- a/FFMpegCore.Extensions.SkiaSharp/FFMpegCore.Extensions.SkiaSharp.csproj +++ b/FFMpegCore.Extensions.SkiaSharp/FFMpegCore.Extensions.SkiaSharp.csproj @@ -3,18 +3,17 @@ true Image extension for FFMpegCore using SkiaSharp - 5.0.0 + 5.0.2 ../nupkg - - + Bump dependencies ffmpeg ffprobe convert video audio mediafile resize analyze muxing skiasharp Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev, Dimitri Vranken true - - + + 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 ebd7c9a..3a45aa5 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,17 +3,16 @@ true Image extension for FFMpegCore using System.Common.Drawing - 5.0.0 + 5.0.2 ../nupkg - - + Bump dependencies 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 cf455c8..ce39c9d 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 @@ namespace FFMpegCore.Test -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 84b863f..8413f84 100644 --- a/FFMpegCore.Test/FFMpegCore.Test.csproj +++ b/FFMpegCore.Test/FFMpegCore.Test.csproj @@ -8,19 +8,19 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 8da9c19..ce23660 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -467,7 +467,7 @@ namespace FFMpegCore.Test } [TestMethod, Timeout(BaseTimeoutMilliseconds)] - public void Video_Snapshot_PersistSnapshot() + public void Video_Snapshot_Png_PersistSnapshot() { using var outputPath = new TemporaryFile("out.png"); var input = FFProbe.Analyse(TestResources.Mp4Video); @@ -480,6 +480,63 @@ namespace FFMpegCore.Test Assert.AreEqual("png", analysis.PrimaryVideoStream!.CodecName); } + [TestMethod, Timeout(BaseTimeoutMilliseconds)] + public void Video_Snapshot_Jpg_PersistSnapshot() + { + using var outputPath = new TemporaryFile("out.jpg"); + var input = FFProbe.Analyse(TestResources.Mp4Video); + + FFMpeg.Snapshot(TestResources.Mp4Video, outputPath); + + var analysis = FFProbe.Analyse(outputPath); + Assert.AreEqual(input.PrimaryVideoStream!.Width, analysis.PrimaryVideoStream!.Width); + Assert.AreEqual(input.PrimaryVideoStream.Height, analysis.PrimaryVideoStream!.Height); + Assert.AreEqual("mjpeg", analysis.PrimaryVideoStream!.CodecName); + } + + [TestMethod, Timeout(BaseTimeoutMilliseconds)] + public void Video_Snapshot_Bmp_PersistSnapshot() + { + using var outputPath = new TemporaryFile("out.bmp"); + var input = FFProbe.Analyse(TestResources.Mp4Video); + + FFMpeg.Snapshot(TestResources.Mp4Video, outputPath); + + var analysis = FFProbe.Analyse(outputPath); + Assert.AreEqual(input.PrimaryVideoStream!.Width, analysis.PrimaryVideoStream!.Width); + Assert.AreEqual(input.PrimaryVideoStream.Height, analysis.PrimaryVideoStream!.Height); + Assert.AreEqual("bmp", analysis.PrimaryVideoStream!.CodecName); + } + + [TestMethod, Timeout(BaseTimeoutMilliseconds)] + public void Video_Snapshot_Webp_PersistSnapshot() + { + using var outputPath = new TemporaryFile("out.webp"); + var input = FFProbe.Analyse(TestResources.Mp4Video); + + FFMpeg.Snapshot(TestResources.Mp4Video, outputPath); + + var analysis = FFProbe.Analyse(outputPath); + Assert.AreEqual(input.PrimaryVideoStream!.Width, analysis.PrimaryVideoStream!.Width); + Assert.AreEqual(input.PrimaryVideoStream.Height, analysis.PrimaryVideoStream!.Height); + Assert.AreEqual("webp", analysis.PrimaryVideoStream!.CodecName); + } + + [TestMethod, Timeout(BaseTimeoutMilliseconds)] + public void Video_Snapshot_Exception_PersistSnapshot() + { + using var outputPath = new TemporaryFile("out.asd"); + + try + { + FFMpeg.Snapshot(TestResources.Mp4Video, outputPath); + } + catch (Exception ex) + { + Assert.IsTrue(ex is ArgumentException); + } + } + [TestMethod, Timeout(BaseTimeoutMilliseconds)] public void Video_Snapshot_Rotated_PersistSnapshot() { diff --git a/FFMpegCore/FFMpeg/Arguments/IDynamicArgument.cs b/FFMpegCore/FFMpeg/Arguments/IDynamicArgument.cs index d44d18d..c7b6b56 100644 --- a/FFMpegCore/FFMpeg/Arguments/IDynamicArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/IDynamicArgument.cs @@ -8,6 +8,6 @@ /// /// //public string GetText(StringBuilder context); - public string GetText(IEnumerable context); + string GetText(IEnumerable context); } } diff --git a/FFMpegCore/FFMpeg/Arguments/MultiInputArgument.cs b/FFMpegCore/FFMpeg/Arguments/MultiInputArgument.cs new file mode 100644 index 0000000..288c761 --- /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/Arguments/VideoFiltersArgument.cs b/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs index f7a9e4a..4a43c9d 100644 --- a/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/VideoFiltersArgument.cs @@ -36,8 +36,8 @@ namespace FFMpegCore.Arguments public interface IVideoFilterArgument { - public string Key { get; } - public string Value { get; } + string Key { get; } + string Value { get; } } public class VideoFilterOptions diff --git a/FFMpegCore/FFMpeg/Enums/Enums.cs b/FFMpegCore/FFMpeg/Enums/Enums.cs index 1f00203..d777ecb 100644 --- a/FFMpegCore/FFMpeg/Enums/Enums.cs +++ b/FFMpegCore/FFMpeg/Enums/Enums.cs @@ -15,9 +15,33 @@ public static Codec LibX265 => FFMpeg.GetCodec("libx265"); public static Codec LibVpx => FFMpeg.GetCodec("libvpx"); public static Codec LibTheora => FFMpeg.GetCodec("libtheora"); - public static Codec Png => FFMpeg.GetCodec("png"); public static Codec MpegTs => FFMpeg.GetCodec("mpegts"); public static Codec LibaomAv1 => FFMpeg.GetCodec("libaom-av1"); + + public static class Image + { + public static Codec Png => FFMpeg.GetCodec("png"); + public static Codec Jpg => FFMpeg.GetCodec("mjpeg"); + public static Codec Bmp => FFMpeg.GetCodec("bmp"); + public static Codec Webp => FFMpeg.GetCodec("webp"); + + public static Codec GetByExtension(string path) + { + var ext = Path.GetExtension(path); + switch (ext) + { + case FileExtension.Image.Png: + return Png; + case FileExtension.Image.Jpg: + return Jpg; + case FileExtension.Image.Bmp: + return Bmp; + case FileExtension.Image.Webp: + return Webp; + default: throw new NotSupportedException($"Unsupported image extension: {ext}"); + } + } + } } public static class AudioCodec diff --git a/FFMpegCore/FFMpeg/Enums/FileExtension.cs b/FFMpegCore/FFMpeg/Enums/FileExtension.cs index f3067ba..386ef33 100644 --- a/FFMpegCore/FFMpeg/Enums/FileExtension.cs +++ b/FFMpegCore/FFMpeg/Enums/FileExtension.cs @@ -10,7 +10,10 @@ "libxvpx" => WebM, "libxtheora" => Ogv, "mpegts" => Ts, - "png" => Png, + "png" => Image.Png, + "jpg" => Image.Jpg, + "bmp" => Image.Bmp, + "webp" => Image.Webp, _ => throw new Exception("The extension for this video type is not defined.") }; } @@ -18,8 +21,16 @@ public static readonly string Ts = VideoType.MpegTs.Extension; public static readonly string Ogv = VideoType.Ogv.Extension; public static readonly string WebM = VideoType.WebM.Extension; - public static readonly string Png = ".png"; public static readonly string Mp3 = ".mp3"; public static readonly string Gif = ".gif"; + + public static class Image + { + public const string Png = ".png"; + public const string Jpg = ".jpg"; + public const string Bmp = ".bmp"; + public const string Webp = ".webp"; + public static readonly List All = [Png, Jpg, Bmp, Webp]; + } } } diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index 820d9fb..cc9f94a 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -20,16 +20,11 @@ namespace FFMpegCore /// Bitmap with the requested snapshot. public static bool Snapshot(string input, string output, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0) { - if (Path.GetExtension(output) != FileExtension.Png) - { - output = Path.Combine(Path.GetDirectoryName(output), Path.GetFileNameWithoutExtension(output) + FileExtension.Png); - } + CheckSnapshotOutputExtension(output, FileExtension.Image.All); var source = FFProbe.Analyse(input); - var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); - return arguments - .OutputToFile(output, true, outputOptions) + return SnapshotProcess(input, output, source, size, captureTime, streamIndex, inputFileIndex) .ProcessSynchronously(); } /// @@ -44,47 +39,55 @@ namespace FFMpegCore /// Bitmap with the requested snapshot. public static async Task SnapshotAsync(string input, string output, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0) { - if (Path.GetExtension(output) != FileExtension.Png) - { - output = Path.Combine(Path.GetDirectoryName(output), Path.GetFileNameWithoutExtension(output) + FileExtension.Png); - } + CheckSnapshotOutputExtension(output, FileExtension.Image.All); var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false); - var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex); - return await arguments - .OutputToFile(output, true, outputOptions) + return await SnapshotProcess(input, output, source, size, captureTime, streamIndex, inputFileIndex) .ProcessAsynchronously(); } public static bool GifSnapshot(string input, string output, Size? size = null, TimeSpan? captureTime = null, TimeSpan? duration = null, int? streamIndex = null) { - if (Path.GetExtension(output)?.ToLower() != FileExtension.Gif) - { - output = Path.Combine(Path.GetDirectoryName(output), Path.GetFileNameWithoutExtension(output) + FileExtension.Gif); - } + CheckSnapshotOutputExtension(output, [FileExtension.Gif]); var source = FFProbe.Analyse(input); - var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildGifSnapshotArguments(input, source, size, captureTime, duration, streamIndex); - return arguments - .OutputToFile(output, true, outputOptions) + return GifSnapshotProcess(input, output, source, size, captureTime, duration, streamIndex) .ProcessSynchronously(); } public static async Task GifSnapshotAsync(string input, string output, Size? size = null, TimeSpan? captureTime = null, TimeSpan? duration = null, int? streamIndex = null) { - if (Path.GetExtension(output)?.ToLower() != FileExtension.Gif) - { - output = Path.Combine(Path.GetDirectoryName(output), Path.GetFileNameWithoutExtension(output) + FileExtension.Gif); - } + CheckSnapshotOutputExtension(output, [FileExtension.Gif]); var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false); + + return await GifSnapshotProcess(input, output, source, size, captureTime, duration, streamIndex) + .ProcessAsynchronously(); + } + + private static FFMpegArgumentProcessor SnapshotProcess(string input, string output, IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0) + { + var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, output, source, size, captureTime, streamIndex, inputFileIndex); + + return arguments.OutputToFile(output, true, outputOptions); + } + + private static FFMpegArgumentProcessor GifSnapshotProcess(string input, string output, IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null, TimeSpan? duration = null, int? streamIndex = null) + { var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildGifSnapshotArguments(input, source, size, captureTime, duration, streamIndex); - return await arguments - .OutputToFile(output, true, outputOptions) - .ProcessAsynchronously(); + return arguments.OutputToFile(output, true, outputOptions); + } + + private static void CheckSnapshotOutputExtension(string output, List extensions) + { + if (!extensions.Contains(Path.GetExtension(output).ToLower())) + { + throw new ArgumentException( + $"Invalid snapshot output extension: {output}, needed: {string.Join(",", FileExtension.Image.All)}"); + } } /// diff --git a/FFMpegCore/FFMpeg/FFMpegArguments.cs b/FFMpegCore/FFMpeg/FFMpegArguments.cs index cfc6d9d..ddb1f72 100644 --- a/FFMpegCore/FFMpeg/FFMpegArguments.cs +++ b/FFMpegCore/FFMpeg/FFMpegArguments.cs @@ -21,6 +21,7 @@ namespace FFMpegCore 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 @@ namespace FFMpegCore 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/SnapshotArgumentBuilder.cs b/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs index 7d83183..3204bcc 100644 --- a/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs +++ b/FFMpegCore/FFMpeg/SnapshotArgumentBuilder.cs @@ -9,6 +9,30 @@ public static class SnapshotArgumentBuilder { public static (FFMpegArguments, Action outputOptions) BuildSnapshotArguments( string input, + string output, + IMediaAnalysis source, + Size? size = null, + TimeSpan? captureTime = null, + int? streamIndex = null, + int inputFileIndex = 0) + { + return BuildSnapshotArguments(input, VideoCodec.Image.GetByExtension(output), source, size, captureTime, streamIndex, inputFileIndex); + } + + public static (FFMpegArguments, Action outputOptions) BuildSnapshotArguments( + string input, + IMediaAnalysis source, + Size? size = null, + TimeSpan? captureTime = null, + int? streamIndex = null, + int inputFileIndex = 0) + { + return BuildSnapshotArguments(input, VideoCodec.Image.Png, source, size, captureTime, streamIndex, inputFileIndex); + } + + private static (FFMpegArguments, Action outputOptions) BuildSnapshotArguments( + string input, + Codec codec, IMediaAnalysis source, Size? size = null, TimeSpan? captureTime = null, @@ -26,7 +50,7 @@ public static class SnapshotArgumentBuilder .Seek(captureTime)), options => options .SelectStream((int)streamIndex, inputFileIndex) - .WithVideoCodec(VideoCodec.Png) + .WithVideoCodec(codec) .WithFrameOutputCount(1) .Resize(size)); } diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index 2c02502..c324d62 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -3,10 +3,15 @@ true A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications - 5.1.0 + 5.2.0 ../nupkg - - + - **Instances and Packages Updates**: Updates to various instances and packages by rosenbjerg. +- **Audio and Video Enhancements**: Additions include a Copy option to Audio Codec and a Crop option to Arguments by brett-baker; video-stream level added to FFProbe analysis by Kaaybi; AV1 support for smaller snapshots and videos by BenediktBertsch; multiple input files support by AddyMills; HDR color properties support added to FFProbe analysis by Tomiscout. +- **System.Text.Json Bump**: Update by Kaaybi. +- **FFMpeg Processors and Utilities**: Modification for handling durations over 24 hours in `FFMpegArgumentProcessor` by alahane-techtel; fix for snapshots with correct width/height from rotated videos by Hagfjall. +- **Feature Additions and Fixes**: Support for multiple outputs and tee muxer by duggaraju; custom ffprob arguments by vfrz; fix for null reference exception with tags container by rosenbjerg; Chapter Modell change by vortex852456; codec copy added to the SaveM3U8Stream method by rpaschoal. +- **Closed and Non-merged Contributions**: Notable closed contributions include JSON source generators usage by onionware-github; Snapshot overload by 3UR; FromRawInput method by pedoc; runtime ffmpeg suite installation by yuqian5; and support for scale_npp by vicwilliam. +- **Miscellaneous Fixes**: Minor readme corrections by NaBian; fix for ffmpeg path issue by devedse. ffmpeg ffprobe convert video audio mediafile resize analyze muxing Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev README.md @@ -17,8 +22,8 @@ - - + + diff --git a/README.md b/README.md index 33f7ddf..4b4a2b2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # [FFMpegCore](https://www.nuget.org/packages/FFMpegCore/) -[![NuGet Badge](https://buildstats.info/nuget/FFMpegCore)](https://www.nuget.org/packages/FFMpegCore/) +[![NuGet Version](https://img.shields.io/nuget/v/FFMpegCore)](https://www.nuget.org/packages/FFMpegCore/) [![GitHub issues](https://img.shields.io/github/issues/rosenbjerg/FFMpegCore)](https://github.com/rosenbjerg/FFMpegCore/issues) [![GitHub stars](https://img.shields.io/github/stars/rosenbjerg/FFMpegCore)](https://github.com/rosenbjerg/FFMpegCore/stargazers) [![GitHub](https://img.shields.io/github/license/rosenbjerg/FFMpegCore)](https://github.com/rosenbjerg/FFMpegCore/blob/master/LICENSE)