diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6946920..5afb8d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,10 +4,18 @@ on: push: branches: - master + paths: + - .github/workflows/ci.yml + - FFMpegCore/** + - FFMpegCore.Test/** pull_request: branches: - master - release + paths: + - .github/workflows/ci.yml + - FFMpegCore/** + - FFMpegCore.Test/** jobs: ci: @@ -22,7 +30,7 @@ jobs: - name: Prepare .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '5.0.x' + dotnet-version: '6.0.x' - name: Prepare FFMpeg uses: FedericoCarboni/setup-ffmpeg@v1 - name: Test with dotnet diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5ef0a4c..7cf1425 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ jobs: - name: Prepare .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '5.0.x' + dotnet-version: '6.0.x' - name: Build solution run: dotnet build --output build -c Release - name: Publish NuGet package diff --git a/FFMpegCore.Examples/FFMpegCore.Examples.csproj b/FFMpegCore.Examples/FFMpegCore.Examples.csproj index f9daae7..68e7b5c 100644 --- a/FFMpegCore.Examples/FFMpegCore.Examples.csproj +++ b/FFMpegCore.Examples/FFMpegCore.Examples.csproj @@ -2,7 +2,7 @@ Exe - net5.0 + net6.0 diff --git a/FFMpegCore.Test/ArgumentBuilderTest.cs b/FFMpegCore.Test/ArgumentBuilderTest.cs index 54620a5..9cf7e39 100644 --- a/FFMpegCore.Test/ArgumentBuilderTest.cs +++ b/FFMpegCore.Test/ArgumentBuilderTest.cs @@ -334,7 +334,7 @@ public void Builder_BuildString_SubtitleHardBurnFilter() .HardBurnSubtitle(SubtitleHardBurnOptions .Create(subtitlePath: "sample.srt") .SetCharacterEncoding("UTF-8") - .SetOriginalSize(1366,768) + .SetOriginalSize(1366, 768) .SetSubtitleIndex(0) .WithStyle(StyleOptions.Create() .WithParameter("FontName", "DejaVu Serif") @@ -479,10 +479,21 @@ public void Builder_BuildString_DynamicAudioNormalizerWithValuesFormat() { var str = FFMpegArguments.FromFileInput("input.mp4") .OutputToFile("output.mp4", false, - opt => opt.WithAudioFilters(filterOptions => filterOptions.DynamicNormalizer(125, 13, 0.9215, 5.124, 0.5458,false,true,true, 0.3333333))) + opt => opt.WithAudioFilters(filterOptions => filterOptions.DynamicNormalizer(125, 13, 0.9215, 5.124, 0.5458, false, true, true, 0.3333333))) .Arguments; Assert.AreEqual("-i \"input.mp4\" -af \"dynaudnorm=f=125:g=13:p=0.92:m=5.1:r=0.5:n=0:c=1:b=1:s=0.3\" \"output.mp4\"", str); } + + [TestMethod] + public void Builder_BuildString_Audible_AAXC_Decryption() + { + var str = FFMpegArguments.FromFileInput("input.aaxc", false, x => x.WithAudibleEncryptionKeys("123", "456")) + .MapMetaData() + .OutputToFile("output.m4b", true, x => x.WithTagVersion(3).DisableChannel(Channel.Video).CopyChannel(Channel.Audio)) + .Arguments; + + Assert.AreEqual("-audible_key 123 -audible_iv 456 -i \"input.aaxc\" -map_metadata 0 -id3v2_version 3 -vn -c:a copy \"output.m4b\" -y", str); + } } } \ No newline at end of file diff --git a/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs b/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs index 8443d0d..6e30999 100644 --- a/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs +++ b/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs @@ -94,5 +94,20 @@ public void Concat_Escape() var arg = new DemuxConcatArgument(new[] { @"Heaven's River\05 - Investigation.m4b" }); arg.Values.Should().BeEquivalentTo(new[] { @"file 'Heaven'\''s River\05 - Investigation.m4b'" }); } + + [TestMethod] + public void Audible_Aaxc_Test() + { + var arg = new AudibleEncryptionKeyArgument("123", "456"); + arg.Text.Should().Be($"-audible_key 123 -audible_iv 456"); + } + + + [TestMethod] + public void Audible_Aax_Test() + { + var arg = new AudibleEncryptionKeyArgument("62689101"); + arg.Text.Should().Be($"-activation_bytes 62689101"); + } } } \ No newline at end of file diff --git a/FFMpegCore.Test/FFMpegCore.Test.csproj b/FFMpegCore.Test/FFMpegCore.Test.csproj index 5d49065..d281c3d 100644 --- a/FFMpegCore.Test/FFMpegCore.Test.csproj +++ b/FFMpegCore.Test/FFMpegCore.Test.csproj @@ -1,7 +1,7 @@  - net5.0 + net6.0 false diff --git a/FFMpegCore.Test/FFProbeTests.cs b/FFMpegCore.Test/FFProbeTests.cs index c2e6e5a..a4d836d 100644 --- a/FFMpegCore.Test/FFProbeTests.cs +++ b/FFMpegCore.Test/FFProbeTests.cs @@ -11,13 +11,6 @@ namespace FFMpegCore.Test [TestClass] public class FFProbeTests { - [TestMethod] - public void Probe_TooLongOutput() - { - Assert.ThrowsException(() => FFProbe.Analyse(TestResources.Mp4Video, 5)); - } - - [TestMethod] public async Task Audio_FromStream_Duration() { diff --git a/FFMpegCore.Test/MetaDataBuilderTests.cs b/FFMpegCore.Test/MetaDataBuilderTests.cs index 5f0a144..747fd9e 100644 --- a/FFMpegCore.Test/MetaDataBuilderTests.cs +++ b/FFMpegCore.Test/MetaDataBuilderTests.cs @@ -4,8 +4,10 @@ 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 @@ -50,5 +52,29 @@ public void TestMetaDataBuilderIntegrity() Assert.IsTrue(serialized.Contains("title=Chapter 01", StringComparison.OrdinalIgnoreCase)); Assert.IsTrue(serialized.Contains("album_artist=Pachelbel", StringComparison.OrdinalIgnoreCase)); } + + [TestMethod] + public void TestMapMetadata() + { + //-i "whaterver0" // index: 0 + //-f concat -safe 0 + //-i "\AppData\Local\Temp\concat_b511f2bf-c4af-4f71-b9bd-24d706bf4861.txt" // index: 1 + //-i "\AppData\Local\Temp\metadata_210d3259-3d5c-43c8-9786-54b5c414fa70.txt" // index: 2 + //-map_metadata 2 + + var text0 = FFMpegArguments.FromFileInput("whaterver0") + .AddMetaData("WhatEver3") + .Text; + + var text1 = FFMpegArguments.FromFileInput("whaterver0") + .AddDemuxConcatInput(new[] { "whaterver", "whaterver1" }) + .AddMetaData("WhatEver3") + .Text; + + + + Assert.IsTrue(Regex.IsMatch(text0, "metadata_[0-9a-f-]+\\.txt\" -map_metadata 1"), "map_metadata index is calculated incorrectly."); + Assert.IsTrue(Regex.IsMatch(text1, "metadata_[0-9a-f-]+\\.txt\" -map_metadata 2"), "map_metadata index is calculated incorrectly."); + } } } diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 0f806d6..262bd75 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -113,13 +113,11 @@ public void Video_ToMP4_Args_Pipe_DifferentImageSizes() }; var videoFramesSource = new RawVideoPipeSource(frames); - var ex = Assert.ThrowsException(() => FFMpegArguments + var ex = Assert.ThrowsException(() => FFMpegArguments .FromPipeInput(videoFramesSource) .OutputToFile(outputFile, false, opt => opt .WithVideoCodec(VideoCodec.LibX264)) .ProcessSynchronously()); - - Assert.IsInstanceOfType(ex.GetBaseException(), typeof(FFMpegStreamFormatException)); } @@ -135,13 +133,11 @@ public async Task Video_ToMP4_Args_Pipe_DifferentImageSizes_Async() }; var videoFramesSource = new RawVideoPipeSource(frames); - var ex = await Assert.ThrowsExceptionAsync(() => FFMpegArguments + var ex = await Assert.ThrowsExceptionAsync(() => FFMpegArguments .FromPipeInput(videoFramesSource) .OutputToFile(outputFile, false, opt => opt .WithVideoCodec(VideoCodec.LibX264)) .ProcessAsynchronously()); - - Assert.IsInstanceOfType(ex.GetBaseException(), typeof(FFMpegStreamFormatException)); } [TestMethod, Timeout(10000)] @@ -156,13 +152,11 @@ public void Video_ToMP4_Args_Pipe_DifferentPixelFormats() }; var videoFramesSource = new RawVideoPipeSource(frames); - var ex = Assert.ThrowsException(() => FFMpegArguments + var ex = Assert.ThrowsException(() => FFMpegArguments .FromPipeInput(videoFramesSource) .OutputToFile(outputFile, false, opt => opt .WithVideoCodec(VideoCodec.LibX264)) .ProcessSynchronously()); - - Assert.IsInstanceOfType(ex.GetBaseException(), typeof(FFMpegStreamFormatException)); } @@ -178,13 +172,11 @@ public async Task Video_ToMP4_Args_Pipe_DifferentPixelFormats_Async() }; var videoFramesSource = new RawVideoPipeSource(frames); - var ex = await Assert.ThrowsExceptionAsync(() => FFMpegArguments + var ex = await Assert.ThrowsExceptionAsync(() => FFMpegArguments .FromPipeInput(videoFramesSource) .OutputToFile(outputFile, false, opt => opt .WithVideoCodec(VideoCodec.LibX264)) .ProcessAsynchronously()); - - Assert.IsInstanceOfType(ex.GetBaseException(), typeof(FFMpegStreamFormatException)); } [TestMethod, Timeout(10000)] @@ -468,7 +460,7 @@ public void Video_Join_Image_Sequence() } }); - var outputFile = new TemporaryFile("out.mp4"); + using var outputFile = new TemporaryFile("out.mp4"); var success = FFMpeg.JoinImageSequence(outputFile, images: imageSet.ToArray()); Assert.IsTrue(success); var result = FFProbe.Analyse(outputFile); @@ -544,7 +536,7 @@ public void Video_OutputsData() .WithVerbosityLevel(VerbosityLevel.Info)) .OutputToFile(outputFile, false, opt => opt .WithDuration(TimeSpan.FromSeconds(2))) - .NotifyOnOutput((_, _) => dataReceived = true) + .NotifyOnError(_ => dataReceived = true) .ProcessSynchronously(); Assert.IsTrue(dataReceived); @@ -596,6 +588,27 @@ public async Task Video_Cancel_Async() Assert.IsFalse(result); } + [TestMethod, Timeout(10000)] + public void Video_Cancel() + { + var outputFile = new TemporaryFile("out.mp4"); + var task = FFMpegArguments + .FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args + .WithCustomArgument("-re") + .ForceFormat("lavfi")) + .OutputToFile(outputFile, false, opt => opt + .WithAudioCodec(AudioCodec.Aac) + .WithVideoCodec(VideoCodec.LibX264) + .WithSpeedPreset(Speed.VeryFast)) + .CancellableThrough(out var cancel); + + Task.Delay(300).ContinueWith((_) => cancel()); + + var result = task.ProcessSynchronously(false); + + Assert.IsFalse(result); + } + [TestMethod, Timeout(10000)] public async Task Video_Cancel_Async_With_Timeout() { @@ -615,11 +628,10 @@ public async Task Video_Cancel_Async_With_Timeout() await Task.Delay(300); cancel(); - var result = await task; + await task; var outputInfo = await FFProbe.AnalyseAsync(outputFile); - Assert.IsTrue(result); Assert.IsNotNull(outputInfo); Assert.AreEqual(320, outputInfo.PrimaryVideoStream!.Width); Assert.AreEqual(240, outputInfo.PrimaryVideoStream.Height); @@ -645,14 +657,58 @@ public async Task Video_Cancel_CancellationToken_Async() .CancellableThrough(cts.Token) .ProcessAsynchronously(false); - await Task.Delay(300); - cts.Cancel(); + cts.CancelAfter(300); var result = await task; Assert.IsFalse(result); } + [TestMethod, Timeout(10000)] + public async Task Video_Cancel_CancellationToken_Async_Throws() + { + var outputFile = new TemporaryFile("out.mp4"); + + var cts = new CancellationTokenSource(); + + var task = FFMpegArguments + .FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args + .WithCustomArgument("-re") + .ForceFormat("lavfi")) + .OutputToFile(outputFile, false, opt => opt + .WithAudioCodec(AudioCodec.Aac) + .WithVideoCodec(VideoCodec.LibX264) + .WithSpeedPreset(Speed.VeryFast)) + .CancellableThrough(cts.Token) + .ProcessAsynchronously(); + + cts.CancelAfter(300); + + await Assert.ThrowsExceptionAsync(() => task); + } + + [TestMethod, Timeout(10000)] + public void Video_Cancel_CancellationToken_Throws() + { + var outputFile = new TemporaryFile("out.mp4"); + + var cts = new CancellationTokenSource(); + + var task = FFMpegArguments + .FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args + .WithCustomArgument("-re") + .ForceFormat("lavfi")) + .OutputToFile(outputFile, false, opt => opt + .WithAudioCodec(AudioCodec.Aac) + .WithVideoCodec(VideoCodec.LibX264) + .WithSpeedPreset(Speed.VeryFast)) + .CancellableThrough(cts.Token); + + cts.CancelAfter(300); + + Assert.ThrowsException(() => task.ProcessSynchronously()); + } + [TestMethod, Timeout(10000)] public async Task Video_Cancel_CancellationToken_Async_With_Timeout() { @@ -671,14 +727,12 @@ public async Task Video_Cancel_CancellationToken_Async_With_Timeout() .CancellableThrough(cts.Token, 8000) .ProcessAsynchronously(false); - await Task.Delay(300); - cts.Cancel(); + cts.CancelAfter(300); - var result = await task; + await task; var outputInfo = await FFProbe.AnalyseAsync(outputFile); - Assert.IsTrue(result); Assert.IsNotNull(outputInfo); Assert.AreEqual(320, outputInfo.PrimaryVideoStream!.Width); Assert.AreEqual(240, outputInfo.PrimaryVideoStream.Height); diff --git a/FFMpegCore/Extend/StringExtensions.cs b/FFMpegCore/Extend/StringExtensions.cs index 29c8d42..7b02089 100644 --- a/FFMpegCore/Extend/StringExtensions.cs +++ b/FFMpegCore/Extend/StringExtensions.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Text; namespace FFMpegCore.Extend diff --git a/FFMpegCore/FFMpeg/Arguments/AudibleEncryptionKeyArgument.cs b/FFMpegCore/FFMpeg/Arguments/AudibleEncryptionKeyArgument.cs new file mode 100644 index 0000000..0f514dc --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/AudibleEncryptionKeyArgument.cs @@ -0,0 +1,28 @@ +namespace FFMpegCore.Arguments +{ + public class AudibleEncryptionKeyArgument : IArgument + { + private readonly bool _aaxcMode; + + private readonly string _key; + private readonly string _iv; + + private readonly string _activationBytes; + + + public AudibleEncryptionKeyArgument(string activationBytes) + { + _activationBytes = activationBytes; + } + + public AudibleEncryptionKeyArgument(string key, string iv) + { + _aaxcMode = true; + + _key = key; + _iv = iv; + } + + public string Text => _aaxcMode ? $"-audible_key {_key} -audible_iv {_iv}" : $"-activation_bytes {_activationBytes}"; + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/ID3V2VersionArgument.cs b/FFMpegCore/FFMpeg/Arguments/ID3V2VersionArgument.cs new file mode 100644 index 0000000..e18d93b --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/ID3V2VersionArgument.cs @@ -0,0 +1,14 @@ +namespace FFMpegCore.Arguments +{ + public class ID3V2VersionArgument : IArgument + { + private readonly int _version; + + public ID3V2VersionArgument(int version) + { + _version = version; + } + + public string Text => $"-id3v2_version {_version}"; + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/IDynamicArgument.cs b/FFMpegCore/FFMpeg/Arguments/IDynamicArgument.cs new file mode 100644 index 0000000..36a504e --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/IDynamicArgument.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Text; + +namespace FFMpegCore.Arguments +{ + public interface IDynamicArgument + { + /// + /// Same as , but this receives the arguments generated before as parameter + /// + /// + /// + //public string GetText(StringBuilder context); + public string GetText(IEnumerable context); + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Arguments/InputPipeArgument.cs b/FFMpegCore/FFMpeg/Arguments/InputPipeArgument.cs index 199d324..d5ba44c 100644 --- a/FFMpegCore/FFMpeg/Arguments/InputPipeArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/InputPipeArgument.cs @@ -1,4 +1,5 @@ -using System.IO.Pipes; +using System; +using System.IO.Pipes; using System.Threading; using System.Threading.Tasks; using FFMpegCore.Pipes; @@ -23,7 +24,7 @@ protected override async Task ProcessDataAsync(CancellationToken token) { await Pipe.WaitForConnectionAsync(token).ConfigureAwait(false); if (!Pipe.IsConnected) - throw new TaskCanceledException(); + throw new OperationCanceledException(); await Writer.WriteAsync(Pipe, token).ConfigureAwait(false); } } diff --git a/FFMpegCore/FFMpeg/Arguments/MapMetadataArgument.cs b/FFMpegCore/FFMpeg/Arguments/MapMetadataArgument.cs new file mode 100644 index 0000000..afec731 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/MapMetadataArgument.cs @@ -0,0 +1,64 @@ +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 +{ + public class MapMetadataArgument : IInputArgument, IDynamicArgument + { + private readonly int? _inputIndex; + + public string Text => GetText(null); + + /// + /// Null means it takes the last input used before this argument + /// + /// + public MapMetadataArgument(int? inputIndex = null) + { + _inputIndex = inputIndex; + } + + public string GetText(IEnumerable? arguments) + { + arguments ??= Enumerable.Empty(); + + var index = 0; + if (_inputIndex is null) + { + index = arguments + .TakeWhile(x => x != this) + .OfType() + .Count(); + + index = Math.Max(index - 1, 0); + } + else + { + index = _inputIndex.Value; + } + + return $"-map_metadata {index}"; + } + + public Task During(CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + public void Post() + { + } + + public void Pre() + { + } + + + } +} diff --git a/FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs b/FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs index 7e9ffc6..89bb1fe 100644 --- a/FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs @@ -1,11 +1,16 @@ -using System; +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 { - public class MetaDataArgument : IInputArgument + public class MetaDataArgument : IInputArgument, IDynamicArgument { private readonly string _metaDataContent; private readonly string _tempFileName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, $"metadata_{Guid.NewGuid()}.txt"); @@ -15,7 +20,7 @@ public MetaDataArgument(string metaDataContent) _metaDataContent = metaDataContent; } - public string Text => $"-i \"{_tempFileName}\" -map_metadata 1"; + public string Text => GetText(null); public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask; @@ -23,5 +28,17 @@ public MetaDataArgument(string metaDataContent) public void Pre() => File.WriteAllText(_tempFileName, _metaDataContent); public void Post() => File.Delete(_tempFileName); + + public string GetText(IEnumerable? arguments) + { + arguments ??= Enumerable.Empty(); + + var index = arguments + .TakeWhile(x => x != this) + .OfType() + .Count(); + + return $"-i \"{_tempFileName}\" -map_metadata {index}"; + } } } diff --git a/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs b/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs index c25df04..ddaab82 100644 --- a/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/PipeArgument.cs @@ -42,14 +42,15 @@ public async Task During(CancellationToken cancellationToken = default) { await ProcessDataAsync(cancellationToken).ConfigureAwait(false); } - catch (TaskCanceledException) + catch (OperationCanceledException) { Debug.WriteLine($"ProcessDataAsync on {GetType().Name} cancelled"); } finally { Debug.WriteLine($"Disconnecting NamedPipeServerStream on {GetType().Name}"); - Pipe?.Disconnect(); + if (Pipe is { IsConnected: true }) + Pipe.Disconnect(); } } diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index 6f0ede3..9e9e0ce 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -8,6 +8,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; +using Instances; namespace FFMpegCore { @@ -246,13 +247,18 @@ public static bool Convert( public static bool PosterWithAudio(string image, string audio, string output) { FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.Mp4); - FFMpegHelper.ConversionSizeExceptionCheck(Image.FromFile(image)); + using (var imageFile = Image.FromFile(image)) + { + FFMpegHelper.ConversionSizeExceptionCheck(imageFile); + } return FFMpegArguments .FromFileInput(image, false, options => options - .Loop(1)) + .Loop(1) + .ForceFormat("image2")) .AddFileInput(audio) .OutputToFile(output, true, options => options + .ForcePixelFormat("yuv420p") .WithVideoCodec(VideoCodec.LibX264) .WithConstantRateFactor(21) .WithAudioBitrate(AudioQuality.Normal) @@ -319,6 +325,7 @@ public static bool JoinImageSequence(string output, double frameRate = 30, param return FFMpegArguments .FromFileInput(Path.Combine(tempFolderName, "%09d.png"), false) .OutputToFile(output, true, options => options + .ForcePixelFormat("yuv420p") .Resize(firstImage.Width, firstImage.Height) .WithFramerate(frameRate)) .ProcessSynchronously(); @@ -417,15 +424,16 @@ internal static IReadOnlyList GetPixelFormatsInternal() FFMpegHelper.RootExceptionCheck(); var list = new List(); - using var instance = new Instances.Instance(GlobalFFOptions.GetFFMpegBinaryPath(), "-pix_fmts"); - instance.DataReceived += (e, args) => + var processArguments = new ProcessArguments(GlobalFFOptions.GetFFMpegBinaryPath(), "-pix_fmts"); + processArguments.OutputDataReceived += (e, data) => { - if (PixelFormat.TryParse(args.Data, out var format)) + if (PixelFormat.TryParse(data, out var format)) list.Add(format); }; - var exitCode = instance.BlockUntilFinished(); - if (exitCode != 0) throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\r\n", instance.OutputData)); + var result = processArguments.StartAndWaitForExit(); + if (result.ExitCode != 0) + throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\r\n", result.OutputData)); return list.AsReadOnly(); } @@ -462,10 +470,10 @@ private static void ParsePartOfCodecs(Dictionary codecs, string a { FFMpegHelper.RootExceptionCheck(); - using var instance = new Instances.Instance(GlobalFFOptions.GetFFMpegBinaryPath(), arguments); - instance.DataReceived += (e, args) => + var processArguments = new ProcessArguments(GlobalFFOptions.GetFFMpegBinaryPath(), arguments); + processArguments.OutputDataReceived += (e, data) => { - var codec = parser(args.Data); + var codec = parser(data); if(codec != null) if (codecs.TryGetValue(codec.Name, out var parentCodec)) parentCodec.Merge(codec); @@ -473,8 +481,8 @@ private static void ParsePartOfCodecs(Dictionary codecs, string a codecs.Add(codec.Name, codec); }; - var exitCode = instance.BlockUntilFinished(); - if (exitCode != 0) throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\r\n", instance.OutputData)); + var result = processArguments.StartAndWaitForExit(); + if (result.ExitCode != 0) throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\r\n", result.OutputData)); } internal static Dictionary GetCodecsInternal() @@ -546,15 +554,15 @@ internal static IReadOnlyList GetContainersFormatsInternal() FFMpegHelper.RootExceptionCheck(); var list = new List(); - using var instance = new Instances.Instance(GlobalFFOptions.GetFFMpegBinaryPath(), "-formats"); - instance.DataReceived += (e, args) => + var instance = new ProcessArguments(GlobalFFOptions.GetFFMpegBinaryPath(), "-formats"); + instance.OutputDataReceived += (e, data) => { - if (ContainerFormat.TryParse(args.Data, out var fmt)) + if (ContainerFormat.TryParse(data, out var fmt)) list.Add(fmt); }; - var exitCode = instance.BlockUntilFinished(); - if (exitCode != 0) throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\r\n", instance.OutputData)); + var result = instance.StartAndWaitForExit(); + if (result.ExitCode != 0) throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\r\n", result.OutputData)); return list.AsReadOnly(); } diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs index ca6628a..7b3da7a 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs @@ -1,5 +1,6 @@ using System; using System.Drawing; + using FFMpegCore.Arguments; using FFMpegCore.Enums; @@ -66,6 +67,11 @@ public FFMpegArgumentOptions WithAudioFilters(Action audioFi public FFMpegArgumentOptions ForcePixelFormat(string pixelFormat) => WithArgument(new ForcePixelFormat(pixelFormat)); public FFMpegArgumentOptions ForcePixelFormat(PixelFormat pixelFormat) => WithArgument(new ForcePixelFormat(pixelFormat)); + public FFMpegArgumentOptions WithAudibleEncryptionKeys(string key, string iv) => WithArgument(new AudibleEncryptionKeyArgument(key, iv)); + public FFMpegArgumentOptions WithAudibleActivationBytes(string activationBytes) => WithArgument(new AudibleEncryptionKeyArgument(activationBytes)); + public FFMpegArgumentOptions WithTagVersion(int id3v2Version = 3) => WithArgument(new ID3V2VersionArgument(id3v2Version)); + + public FFMpegArgumentOptions WithArgument(IArgument argument) { Arguments.Add(argument); diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs index fdbdcc8..43ace4d 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs @@ -18,7 +18,8 @@ public class FFMpegArgumentProcessor private readonly FFMpegArguments _ffMpegArguments; private Action? _onPercentageProgress; private Action? _onTimeProgress; - private Action? _onOutput; + private Action? _onOutput; + private Action? _onError; private TimeSpan? _totalTimespan; internal FFMpegArgumentProcessor(FFMpegArguments ffMpegArguments) @@ -57,11 +58,16 @@ public FFMpegArgumentProcessor NotifyOnProgress(Action onTimeProgress) /// Register action that will be invoked during the ffmpeg processing, when a line is output /// /// - public FFMpegArgumentProcessor NotifyOnOutput(Action onOutput) + public FFMpegArgumentProcessor NotifyOnOutput(Action onOutput) { _onOutput = onOutput; return this; } + public FFMpegArgumentProcessor NotifyOnError(Action onError) + { + _onError = onError; + return this; + } public FFMpegArgumentProcessor CancellableThrough(out Action cancel, int timeout = 0) { cancel = () => CancelEvent?.Invoke(this, timeout); @@ -80,85 +86,83 @@ public FFMpegArgumentProcessor Configure(Action configureOptions) public bool ProcessSynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null) { var options = GetConfiguredOptions(ffMpegOptions); - using var instance = PrepareInstance(options, out var cancellationTokenSource); + var processArguments = PrepareProcessArguments(options, out var cancellationTokenSource); - void OnCancelEvent(object sender, int timeout) - { - instance.SendInput("q"); - - if (!cancellationTokenSource.Token.WaitHandle.WaitOne(timeout, true)) - { - cancellationTokenSource.Cancel(); - instance.Started = false; - } - } - CancelEvent += OnCancelEvent; - instance.Exited += delegate { cancellationTokenSource.Cancel(); }; - - var errorCode = -1; + + IProcessResult? processResult = null; try { - errorCode = Process(instance, cancellationTokenSource).ConfigureAwait(false).GetAwaiter().GetResult(); + processResult = Process(processArguments, cancellationTokenSource).ConfigureAwait(false).GetAwaiter().GetResult(); } - catch (Exception e) + catch (OperationCanceledException) { - if (!HandleException(throwOnError, e, instance.ErrorData)) return false; - } - finally - { - CancelEvent -= OnCancelEvent; + if (throwOnError) + throw; } - return HandleCompletion(throwOnError, errorCode, instance.ErrorData); + return HandleCompletion(throwOnError, processResult?.ExitCode ?? -1, processResult?.ErrorData ?? Array.Empty()); } public async Task ProcessAsynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null) { var options = GetConfiguredOptions(ffMpegOptions); - using var instance = PrepareInstance(options, out var cancellationTokenSource); + var processArguments = PrepareProcessArguments(options, out var cancellationTokenSource); + + IProcessResult? processResult = null; + try + { + processResult = await Process(processArguments, cancellationTokenSource).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + if (throwOnError) + throw; + } + + return HandleCompletion(throwOnError, processResult?.ExitCode ?? -1, processResult?.ErrorData ?? Array.Empty()); + } + private async Task Process(ProcessArguments processArguments, CancellationTokenSource cancellationTokenSource) + { + IProcessResult processResult = null!; + + _ffMpegArguments.Pre(); + + using var instance = processArguments.Start(); + var cancelled = false; void OnCancelEvent(object sender, int timeout) { + cancelled = true; instance.SendInput("q"); if (!cancellationTokenSource.Token.WaitHandle.WaitOne(timeout, true)) { cancellationTokenSource.Cancel(); - instance.Started = false; + instance.Kill(); } } CancelEvent += OnCancelEvent; - var errorCode = -1; try { - errorCode = await Process(instance, cancellationTokenSource).ConfigureAwait(false); - } - catch (Exception e) - { - if (!HandleException(throwOnError, e, instance.ErrorData)) return false; + await Task.WhenAll(instance.WaitForExitAsync().ContinueWith(t => + { + processResult = t.Result; + cancellationTokenSource.Cancel(); + _ffMpegArguments.Post(); + }), _ffMpegArguments.During(cancellationTokenSource.Token)).ConfigureAwait(false); + + if (cancelled) + { + throw new OperationCanceledException("ffmpeg processing was cancelled"); + } + + return processResult; } finally { CancelEvent -= OnCancelEvent; } - - return HandleCompletion(throwOnError, errorCode, instance.ErrorData); - } - - private async Task Process(Instance instance, CancellationTokenSource cancellationTokenSource) - { - var errorCode = -1; - - _ffMpegArguments.Pre(); - await Task.WhenAll(instance.FinishedRunning().ContinueWith(t => - { - errorCode = t.Result; - cancellationTokenSource.Cancel(); - _ffMpegArguments.Post(); - }), _ffMpegArguments.During(cancellationTokenSource.Token)).ConfigureAwait(false); - - return errorCode; } private bool HandleCompletion(bool throwOnError, int exitCode, IReadOnlyList errorData) @@ -184,7 +188,7 @@ internal FFOptions GetConfiguredOptions(FFOptions? ffOptions) return options; } - private Instance PrepareInstance(FFOptions ffOptions, + private ProcessArguments PrepareProcessArguments(FFOptions ffOptions, out CancellationTokenSource cancellationTokenSource) { FFMpegHelper.RootExceptionCheck(); @@ -197,30 +201,29 @@ private Instance PrepareInstance(FFOptions ffOptions, StandardErrorEncoding = ffOptions.Encoding, WorkingDirectory = ffOptions.WorkingDirectory }; - var instance = new Instance(startInfo); + var processArguments = new ProcessArguments(startInfo); cancellationTokenSource = new CancellationTokenSource(); if (_onOutput != null || _onTimeProgress != null || (_onPercentageProgress != null && _totalTimespan != null)) - instance.DataReceived += OutputData; + processArguments.OutputDataReceived += OutputData; + + if (_onError != null) + processArguments.ErrorDataReceived += ErrorData; - return instance; + return processArguments; } - - private static bool HandleException(bool throwOnError, Exception e, IReadOnlyList errorData) + private void ErrorData(object sender, string msg) { - if (!throwOnError) - return false; - - throw new FFMpegException(FFMpegExceptionType.Process, "Exception thrown during processing", e, string.Join("\n", errorData)); + _onError?.Invoke(msg); } - private void OutputData(object sender, (DataType Type, string Data) msg) + private void OutputData(object sender, string msg) { - Debug.WriteLine(msg.Data); - _onOutput?.Invoke(msg.Data, msg.Type); + Debug.WriteLine(msg); + _onOutput?.Invoke(msg); - var match = ProgressRegex.Match(msg.Data); + var match = ProgressRegex.Match(msg); if (!match.Success) return; var processed = TimeSpan.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); diff --git a/FFMpegCore/FFMpeg/FFMpegArguments.cs b/FFMpegCore/FFMpeg/FFMpegArguments.cs index 6c9784d..c100c85 100644 --- a/FFMpegCore/FFMpeg/FFMpegArguments.cs +++ b/FFMpegCore/FFMpeg/FFMpegArguments.cs @@ -2,8 +2,10 @@ 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.Builders.MetaData; using FFMpegCore.Pipes; @@ -13,10 +15,16 @@ namespace FFMpegCore public sealed class FFMpegArguments : FFMpegArgumentsBase { private readonly FFMpegGlobalArguments _globalArguments = new FFMpegGlobalArguments(); - + private FFMpegArguments() { } - public string Text => string.Join(" ", _globalArguments.Arguments.Concat(Arguments).Select(arg => arg.Text)); + public string Text => GetText(); + + private string GetText() + { + var allArguments = _globalArguments.Arguments.Concat(Arguments).ToArray(); + return string.Join(" ", allArguments.Select(arg => arg is IDynamicArgument dynArg ? dynArg.GetText(allArguments) : arg.Text)); + } 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); @@ -26,7 +34,7 @@ private FFMpegArguments() { } 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); @@ -42,6 +50,13 @@ public FFMpegArguments WithGlobalOptions(Action configure public FFMpegArguments AddMetaData(string content, Action? addArguments = null) => WithInput(new MetaDataArgument(content), addArguments); public FFMpegArguments AddMetaData(IReadOnlyMetaData metaData, Action? addArguments = null) => WithInput(new MetaDataArgument(MetaDataSerializer.Instance.Serialize(metaData)), addArguments); + + /// + /// Maps the metadata of the given stream + /// + /// null means, the previous input will be used + public FFMpegArguments MapMetaData(int? inputIndex = null, Action? addArguments = null) => WithInput(new MapMetadataArgument(inputIndex), addArguments); + private FFMpegArguments WithInput(IInputArgument inputArgument, Action? addArguments) { var arguments = new FFMpegArgumentOptions(); @@ -81,4 +96,4 @@ internal void Post() argument.Post(); } } -} \ No newline at end of file +} diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj index afabd90..a0464c7 100644 --- a/FFMpegCore/FFMpegCore.csproj +++ b/FFMpegCore/FFMpegCore.csproj @@ -32,9 +32,9 @@ - + - + diff --git a/FFMpegCore/FFProbe/AudioStream.cs b/FFMpegCore/FFProbe/AudioStream.cs index d6f4b33..50c5572 100644 --- a/FFMpegCore/FFProbe/AudioStream.cs +++ b/FFMpegCore/FFProbe/AudioStream.cs @@ -2,9 +2,9 @@ { public class AudioStream : MediaStream { - public int Channels { get; internal set; } - public string ChannelLayout { get; internal set; } = null!; - public int SampleRateHz { get; internal set; } - public string Profile { get; internal set; } = null!; + public int Channels { get; set; } + public string ChannelLayout { get; set; } = null!; + public int SampleRateHz { get; set; } + public string Profile { get; set; } = null!; } } \ No newline at end of file diff --git a/FFMpegCore/FFProbe/FFProbe.cs b/FFMpegCore/FFProbe/FFProbe.cs index 36f050c..21cf3af 100644 --- a/FFMpegCore/FFProbe/FFProbe.cs +++ b/FFMpegCore/FFProbe/FFProbe.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using System.IO; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using FFMpegCore.Arguments; using FFMpegCore.Exceptions; @@ -13,61 +14,55 @@ namespace FFMpegCore { public static class FFProbe { - public static IMediaAnalysis Analyse(string filePath, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) + public static IMediaAnalysis Analyse(string filePath, FFOptions? ffOptions = null) { - if (!File.Exists(filePath)) - throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'"); + ThrowIfInputFileDoesNotExist(filePath); - using var instance = PrepareStreamAnalysisInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); - var exitCode = instance.BlockUntilFinished(); - if (exitCode != 0) - throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({exitCode} - {string.Join("\n", instance.ErrorData)})", null, string.Join("\n", instance.ErrorData)); + var processArguments = PrepareStreamAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current); + var result = processArguments.StartAndWaitForExit(); + ThrowIfExitCodeNotZero(result); - return ParseOutput(instance); + return ParseOutput(result); } - public static FFProbeFrames GetFrames(string filePath, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) + + public static FFProbeFrames GetFrames(string filePath, FFOptions? ffOptions = null) { - if (!File.Exists(filePath)) - throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'"); + ThrowIfInputFileDoesNotExist(filePath); - using var instance = PrepareFrameAnalysisInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); - var exitCode = instance.BlockUntilFinished(); - if (exitCode != 0) - throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({exitCode} - {string.Join("\n", instance.ErrorData)})", null, string.Join("\n", instance.ErrorData)); + var instance = PrepareFrameAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current); + var result = instance.StartAndWaitForExit(); + ThrowIfExitCodeNotZero(result); - return ParseFramesOutput(instance); + return ParseFramesOutput(result); } - public static FFProbePackets GetPackets(string filePath, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) + public static FFProbePackets GetPackets(string filePath, FFOptions? ffOptions = null) { - if (!File.Exists(filePath)) - throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'"); + ThrowIfInputFileDoesNotExist(filePath); - using var instance = PreparePacketAnalysisInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); - var exitCode = instance.BlockUntilFinished(); - if (exitCode != 0) - throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({exitCode} - {string.Join("\n", instance.ErrorData)})", null, string.Join("\n", instance.ErrorData)); + var instance = PreparePacketAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current); + var result = instance.StartAndWaitForExit(); + ThrowIfExitCodeNotZero(result); - return ParsePacketsOutput(instance); + return ParsePacketsOutput(result); } - public static IMediaAnalysis Analyse(Uri uri, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) + public static IMediaAnalysis Analyse(Uri uri, FFOptions? ffOptions = null) { - using var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current); - var exitCode = instance.BlockUntilFinished(); - if (exitCode != 0) - throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({exitCode} - {string.Join("\n", instance.ErrorData)})", null, string.Join("\n", instance.ErrorData)); + var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current); + var result = instance.StartAndWaitForExit(); + ThrowIfExitCodeNotZero(result); - return ParseOutput(instance); + return ParseOutput(result); } - public static IMediaAnalysis Analyse(Stream stream, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) + public static IMediaAnalysis Analyse(Stream stream, FFOptions? ffOptions = null) { var streamPipeSource = new StreamPipeSource(stream); var pipeArgument = new InputPipeArgument(streamPipeSource); - using var instance = PrepareStreamAnalysisInstance(pipeArgument.PipePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); + var instance = PrepareStreamAnalysisInstance(pipeArgument.PipePath, ffOptions ?? GlobalFFOptions.Current); pipeArgument.Pre(); - var task = instance.FinishedRunning(); + var task = instance.StartAndWaitForExitAsync(); try { pipeArgument.During().ConfigureAwait(false).GetAwaiter().GetResult(); @@ -77,65 +72,60 @@ public static IMediaAnalysis Analyse(Stream stream, int outputCapacity = int.Max { pipeArgument.Post(); } - var exitCode = task.ConfigureAwait(false).GetAwaiter().GetResult(); - if (exitCode != 0) - throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({exitCode} - {string.Join("\n", instance.ErrorData)})", null, string.Join("\n", instance.ErrorData)); + var result = task.ConfigureAwait(false).GetAwaiter().GetResult(); + ThrowIfExitCodeNotZero(result); - return ParseOutput(instance); + return ParseOutput(result); } - public static async Task AnalyseAsync(string filePath, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) + + public static async Task AnalyseAsync(string filePath, FFOptions? ffOptions = null, CancellationToken cancellationToken = default) { - if (!File.Exists(filePath)) - throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'"); + ThrowIfInputFileDoesNotExist(filePath); - using var instance = PrepareStreamAnalysisInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); - var exitCode = await instance.FinishedRunning().ConfigureAwait(false); - if (exitCode != 0) - throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({exitCode} - {string.Join("\n", instance.ErrorData)})", null, string.Join("\n", instance.ErrorData)); + var instance = PrepareStreamAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current); + var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); + ThrowIfExitCodeNotZero(result); - return ParseOutput(instance); + return ParseOutput(result); } - public static async Task GetFramesAsync(string filePath, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) + public static async Task GetFramesAsync(string filePath, FFOptions? ffOptions = null, CancellationToken cancellationToken = default) { - if (!File.Exists(filePath)) - throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'"); + ThrowIfInputFileDoesNotExist(filePath); - using var instance = PrepareFrameAnalysisInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); - await instance.FinishedRunning().ConfigureAwait(false); - return ParseFramesOutput(instance); + var instance = PrepareFrameAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current); + var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); + return ParseFramesOutput(result); } - public static async Task GetPacketsAsync(string filePath, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) + public static async Task GetPacketsAsync(string filePath, FFOptions? ffOptions = null, CancellationToken cancellationToken = default) { - if (!File.Exists(filePath)) - throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'"); + ThrowIfInputFileDoesNotExist(filePath); - using var instance = PreparePacketAnalysisInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); - await instance.FinishedRunning().ConfigureAwait(false); - return ParsePacketsOutput(instance); + var instance = PreparePacketAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current); + var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); + return ParsePacketsOutput(result); } - public static async Task AnalyseAsync(Uri uri, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) + public static async Task AnalyseAsync(Uri uri, FFOptions? ffOptions = null, CancellationToken cancellationToken = default) { - using var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current); - var exitCode = await instance.FinishedRunning().ConfigureAwait(false); - if (exitCode != 0) - throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({exitCode} - {string.Join("\n", instance.ErrorData)})", null, string.Join("\n", instance.ErrorData)); + var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current); + var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); + ThrowIfExitCodeNotZero(result); - return ParseOutput(instance); + return ParseOutput(result); } - public static async Task AnalyseAsync(Stream stream, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) + public static async Task AnalyseAsync(Stream stream, FFOptions? ffOptions = null, CancellationToken cancellationToken = default) { var streamPipeSource = new StreamPipeSource(stream); var pipeArgument = new InputPipeArgument(streamPipeSource); - using var instance = PrepareStreamAnalysisInstance(pipeArgument.PipePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); + var instance = PrepareStreamAnalysisInstance(pipeArgument.PipePath, ffOptions ?? GlobalFFOptions.Current); pipeArgument.Pre(); - var task = instance.FinishedRunning(); + var task = instance.StartAndWaitForExitAsync(cancellationToken); try { - await pipeArgument.During().ConfigureAwait(false); + await pipeArgument.During(cancellationToken).ConfigureAwait(false); } catch(IOException) { @@ -144,15 +134,14 @@ public static async Task AnalyseAsync(Stream stream, int outputC { pipeArgument.Post(); } - var exitCode = await task.ConfigureAwait(false); - if (exitCode != 0) - throw new FFProbeProcessException($"ffprobe exited with non-zero exit-code ({exitCode} - {string.Join("\n", instance.ErrorData)})", instance.ErrorData); + var result = await task.ConfigureAwait(false); + ThrowIfExitCodeNotZero(result); pipeArgument.Post(); - return ParseOutput(instance); + return ParseOutput(result); } - private static IMediaAnalysis ParseOutput(Instance instance) + private static IMediaAnalysis ParseOutput(IProcessResult instance) { var json = string.Join(string.Empty, instance.OutputData); var ffprobeAnalysis = JsonSerializer.Deserialize(json, new JsonSerializerOptions @@ -163,9 +152,10 @@ private static IMediaAnalysis ParseOutput(Instance instance) if (ffprobeAnalysis?.Format == null) throw new FormatNullException(); + ffprobeAnalysis.ErrorData = instance.ErrorData; return new MediaAnalysis(ffprobeAnalysis); } - private static FFProbeFrames ParseFramesOutput(Instance instance) + private static FFProbeFrames ParseFramesOutput(IProcessResult instance) { var json = string.Join(string.Empty, instance.OutputData); var ffprobeAnalysis = JsonSerializer.Deserialize(json, new JsonSerializerOptions @@ -174,10 +164,10 @@ private static FFProbeFrames ParseFramesOutput(Instance instance) NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString | System.Text.Json.Serialization.JsonNumberHandling.WriteAsString }) ; - return ffprobeAnalysis; + return ffprobeAnalysis!; } - private static FFProbePackets ParsePacketsOutput(Instance instance) + private static FFProbePackets ParsePacketsOutput(IProcessResult instance) { var json = string.Join(string.Empty, instance.OutputData); var ffprobeAnalysis = JsonSerializer.Deserialize(json, new JsonSerializerOptions @@ -186,29 +176,44 @@ private static FFProbePackets ParsePacketsOutput(Instance instance) NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString | System.Text.Json.Serialization.JsonNumberHandling.WriteAsString }) ; - return ffprobeAnalysis; + return ffprobeAnalysis!; } + private static void ThrowIfInputFileDoesNotExist(string filePath) + { + if (!File.Exists(filePath)) + { + throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'"); + } + } - private static Instance PrepareStreamAnalysisInstance(string filePath, int outputCapacity, FFOptions ffOptions) - => PrepareInstance($"-loglevel error -print_format json -show_format -sexagesimal -show_streams \"{filePath}\"", outputCapacity, ffOptions); - private static Instance PrepareFrameAnalysisInstance(string filePath, int outputCapacity, FFOptions ffOptions) - => PrepareInstance($"-loglevel error -print_format json -show_frames -v quiet -sexagesimal \"{filePath}\"", outputCapacity, ffOptions); - private static Instance PreparePacketAnalysisInstance(string filePath, int outputCapacity, FFOptions ffOptions) - => PrepareInstance($"-loglevel error -print_format json -show_packets -v quiet -sexagesimal \"{filePath}\"", outputCapacity, ffOptions); + private static void ThrowIfExitCodeNotZero(IProcessResult result) + { + if (result.ExitCode != 0) + { + var message = $"ffprobe exited with non-zero exit-code ({result.ExitCode} - {string.Join("\n", result.ErrorData)})"; + throw new FFMpegException(FFMpegExceptionType.Process, message, null, string.Join("\n", result.ErrorData)); + } + } + + private static ProcessArguments PrepareStreamAnalysisInstance(string filePath, FFOptions ffOptions) + => PrepareInstance($"-loglevel error -print_format json -show_format -sexagesimal -show_streams \"{filePath}\"", ffOptions); + private static ProcessArguments PrepareFrameAnalysisInstance(string filePath, FFOptions ffOptions) + => 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 Instance PrepareInstance(string arguments, int outputCapacity, FFOptions ffOptions) + private static ProcessArguments PrepareInstance(string arguments, FFOptions ffOptions) { FFProbeHelper.RootExceptionCheck(); FFProbeHelper.VerifyFFProbeExists(ffOptions); - var startInfo = new ProcessStartInfo(GlobalFFOptions.GetFFProbeBinaryPath(), arguments) + var startInfo = new ProcessStartInfo(GlobalFFOptions.GetFFProbeBinaryPath(ffOptions), arguments) { StandardOutputEncoding = ffOptions.Encoding, StandardErrorEncoding = ffOptions.Encoding, WorkingDirectory = ffOptions.WorkingDirectory }; - var instance = new Instance(startInfo) { DataBufferCapacity = outputCapacity }; - return instance; + return new ProcessArguments(startInfo); } } } diff --git a/FFMpegCore/FFProbe/FFProbeAnalysis.cs b/FFMpegCore/FFProbe/FFProbeAnalysis.cs index 2177307..cbbb9fd 100644 --- a/FFMpegCore/FFProbe/FFProbeAnalysis.cs +++ b/FFMpegCore/FFProbe/FFProbeAnalysis.cs @@ -10,6 +10,9 @@ public class FFProbeAnalysis [JsonPropertyName("format")] public Format Format { get; set; } = null!; + + [JsonIgnore] + public IReadOnlyList ErrorData { get; set; } } public class FFProbeStream : ITagsContainer, IDispositionContainer diff --git a/FFMpegCore/FFProbe/FrameAnalysis.cs b/FFMpegCore/FFProbe/FrameAnalysis.cs index a22cd24..08e5037 100644 --- a/FFMpegCore/FFProbe/FrameAnalysis.cs +++ b/FFMpegCore/FFProbe/FrameAnalysis.cs @@ -6,7 +6,7 @@ namespace FFMpegCore public class FFProbeFrameAnalysis { [JsonPropertyName("media_type")] - public string MediaType { get; set; } + public string MediaType { get; set; } = null!; [JsonPropertyName("stream_index")] public int StreamIndex { get; set; } @@ -18,25 +18,25 @@ public class FFProbeFrameAnalysis public long PacketPts { get; set; } [JsonPropertyName("pkt_pts_time")] - public string PacketPtsTime { get; set; } + public string PacketPtsTime { get; set; } = null!; [JsonPropertyName("pkt_dts")] public long PacketDts { get; set; } [JsonPropertyName("pkt_dts_time")] - public string PacketDtsTime { get; set; } + 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; } + public string BestEffortTimestampTime { get; set; } = null!; [JsonPropertyName("pkt_duration")] public int PacketDuration { get; set; } [JsonPropertyName("pkt_duration_time")] - public string PacketDurationTime { get; set; } + public string PacketDurationTime { get; set; } = null!; [JsonPropertyName("pkt_pos")] public long PacketPos { get; set; } @@ -51,10 +51,10 @@ public class FFProbeFrameAnalysis public long Height { get; set; } [JsonPropertyName("pix_fmt")] - public string PixelFormat { get; set; } + public string PixelFormat { get; set; } = null!; [JsonPropertyName("pict_type")] - public string PictureType { get; set; } + public string PictureType { get; set; } = null!; [JsonPropertyName("coded_picture_number")] public long CodedPictureNumber { get; set; } @@ -72,12 +72,12 @@ public class FFProbeFrameAnalysis public int RepeatPicture { get; set; } [JsonPropertyName("chroma_location")] - public string ChromaLocation { get; set; } + public string ChromaLocation { get; set; } = null!; } public class FFProbeFrames { [JsonPropertyName("frames")] - public List Frames { get; set; } + public List Frames { get; set; } = null!; } } diff --git a/FFMpegCore/FFProbe/IMediaAnalysis.cs b/FFMpegCore/FFProbe/IMediaAnalysis.cs index 7be3b20..5884f74 100644 --- a/FFMpegCore/FFProbe/IMediaAnalysis.cs +++ b/FFMpegCore/FFProbe/IMediaAnalysis.cs @@ -13,5 +13,6 @@ public interface IMediaAnalysis List VideoStreams { get; } List AudioStreams { get; } List SubtitleStreams { get; } + IReadOnlyList ErrorData { get; } } } diff --git a/FFMpegCore/FFProbe/MediaAnalysis.cs b/FFMpegCore/FFProbe/MediaAnalysis.cs index a2db068..e1fbd1d 100644 --- a/FFMpegCore/FFProbe/MediaAnalysis.cs +++ b/FFMpegCore/FFProbe/MediaAnalysis.cs @@ -13,6 +13,7 @@ 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(); } private MediaFormat ParseFormat(Format analysisFormat) @@ -25,7 +26,7 @@ private MediaFormat ParseFormat(Format analysisFormat) StreamCount = analysisFormat.NbStreams, ProbeScore = analysisFormat.ProbeScore, BitRate = long.Parse(analysisFormat.BitRate ?? "0"), - Tags = analysisFormat.Tags, + Tags = analysisFormat.Tags.ToCaseInsensitive(), }; } @@ -45,7 +46,8 @@ private MediaFormat ParseFormat(Format analysisFormat) public List VideoStreams { get; } public List AudioStreams { get; } public List SubtitleStreams { get; } - + public IReadOnlyList ErrorData { get; } + private VideoStream ParseVideoStream(FFProbeStream stream) { return new VideoStream @@ -68,7 +70,7 @@ private VideoStream ParseVideoStream(FFProbeStream stream) Rotation = (int)float.Parse(stream.GetRotate() ?? "0"), Language = stream.GetLanguage(), Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition), - Tags = stream.Tags, + Tags = stream.Tags.ToCaseInsensitive(), }; } @@ -89,7 +91,7 @@ private AudioStream ParseAudioStream(FFProbeStream stream) Profile = stream.Profile, Language = stream.GetLanguage(), Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition), - Tags = stream.Tags, + Tags = stream.Tags.ToCaseInsensitive(), }; } @@ -104,15 +106,20 @@ private SubtitleStream ParseSubtitleStream(FFProbeStream stream) Duration = MediaAnalysisUtils.ParseDuration(stream), Language = stream.GetLanguage(), Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition), - Tags = stream.Tags, + 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); + internal static Dictionary? ToCaseInsensitive(this Dictionary? dictionary) + { + return dictionary?.ToDictionary(tag => tag.Key, tag => tag.Value, StringComparer.OrdinalIgnoreCase) ?? new Dictionary(); + } public static double DivideRatio((double, double) ratio) => ratio.Item1 / ratio.Item2; public static (int, int) ParseRatioInt(string input, char separator) @@ -183,7 +190,7 @@ public static TimeSpan ParseDuration(FFProbeStream ffProbeStream) return null; } - var result = new Dictionary(disposition.Count); + var result = new Dictionary(disposition.Count, StringComparer.Ordinal); foreach (var pair in disposition) { diff --git a/FFMpegCore/FFProbe/MediaStream.cs b/FFMpegCore/FFProbe/MediaStream.cs index 68bc78f..ffab04b 100644 --- a/FFMpegCore/FFProbe/MediaStream.cs +++ b/FFMpegCore/FFProbe/MediaStream.cs @@ -5,18 +5,18 @@ namespace FFMpegCore { - public class MediaStream + public abstract class MediaStream { - public int Index { get; internal set; } - public string CodecName { get; internal set; } = null!; - public string CodecLongName { get; internal set; } = null!; + public int Index { get; set; } + public string CodecName { get; set; } = null!; + public string CodecLongName { get; set; } = null!; public string CodecTagString { get; set; } = null!; public string CodecTag { get; set; } = null!; - public long BitRate { get; internal set; } - public TimeSpan Duration { get; internal set; } - public string? Language { get; internal set; } - public Dictionary? Disposition { get; internal set; } - public Dictionary? Tags { get; internal set; } + public long BitRate { get; set; } + public TimeSpan Duration { get; set; } + public string? Language { get; set; } + public Dictionary? Disposition { get; set; } + public Dictionary? Tags { get; set; } public Codec GetCodecInfo() => FFMpeg.GetCodec(CodecName); } diff --git a/FFMpegCore/FFProbe/PacketAnalysis.cs b/FFMpegCore/FFProbe/PacketAnalysis.cs index d4da0f5..babe403 100644 --- a/FFMpegCore/FFProbe/PacketAnalysis.cs +++ b/FFMpegCore/FFProbe/PacketAnalysis.cs @@ -6,7 +6,7 @@ namespace FFMpegCore public class FFProbePacketAnalysis { [JsonPropertyName("codec_type")] - public string CodecType { get; set; } + public string CodecType { get; set; } = null!; [JsonPropertyName("stream_index")] public int StreamIndex { get; set; } @@ -15,19 +15,19 @@ public class FFProbePacketAnalysis public long Pts { get; set; } [JsonPropertyName("pts_time")] - public string PtsTime { get; set; } + public string PtsTime { get; set; } = null!; [JsonPropertyName("dts")] public long Dts { get; set; } [JsonPropertyName("dts_time")] - public string DtsTime { get; set; } + public string DtsTime { get; set; } = null!; [JsonPropertyName("duration")] public int Duration { get; set; } [JsonPropertyName("duration_time")] - public string DurationTime { get; set; } + public string DurationTime { get; set; } = null!; [JsonPropertyName("size")] public int Size { get; set; } @@ -36,12 +36,12 @@ public class FFProbePacketAnalysis public long Pos { get; set; } [JsonPropertyName("flags")] - public string Flags { get; set; } + public string Flags { get; set; } = null!; } public class FFProbePackets { [JsonPropertyName("packets")] - public List Packets { get; set; } + public List Packets { get; set; } = null!; } } diff --git a/FFMpegCore/FFProbe/ProcessArgumentsExtensions.cs b/FFMpegCore/FFProbe/ProcessArgumentsExtensions.cs new file mode 100644 index 0000000..1647e9b --- /dev/null +++ b/FFMpegCore/FFProbe/ProcessArgumentsExtensions.cs @@ -0,0 +1,20 @@ +using System.Threading; +using System.Threading.Tasks; +using Instances; + +namespace FFMpegCore +{ + public static class ProcessArgumentsExtensions + { + public static IProcessResult StartAndWaitForExit(this ProcessArguments processArguments) + { + using var instance = processArguments.Start(); + return instance.WaitForExit(); + } + public static async Task StartAndWaitForExitAsync(this ProcessArguments processArguments, CancellationToken cancellationToken = default) + { + using var instance = processArguments.Start(); + return await instance.WaitForExitAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/FFMpegCore/FFProbe/VideoStream.cs b/FFMpegCore/FFProbe/VideoStream.cs index 0bcfc09..a07cdd9 100644 --- a/FFMpegCore/FFProbe/VideoStream.cs +++ b/FFMpegCore/FFProbe/VideoStream.cs @@ -4,15 +4,16 @@ namespace FFMpegCore { public class VideoStream : MediaStream { - public double AvgFrameRate { get; internal set; } - public int BitsPerRawSample { get; internal set; } - public (int Width, int Height) DisplayAspectRatio { get; internal set; } - public string Profile { get; internal set; } = null!; - public int Width { get; internal set; } - public int Height { get; internal set; } - public double FrameRate { get; internal set; } - public string PixelFormat { get; internal set; } = null!; + public double AvgFrameRate { get; set; } + public int BitsPerRawSample { get; set; } + public (int Width, int Height) DisplayAspectRatio { get; set; } + public string Profile { get; set; } = null!; + public int Width { get; set; } + public int Height { get; set; } + public double FrameRate { get; set; } + public string PixelFormat { get; set; } = null!; public int Rotation { get; set; } + public double AverageFrameRate { get; set; } public PixelFormat GetPixelFormatInfo() => FFMpeg.GetPixelFormat(PixelFormat); } diff --git a/FFMpegCore/Helpers/FFMpegHelper.cs b/FFMpegCore/Helpers/FFMpegHelper.cs index 12e52c3..cb3b4cf 100644 --- a/FFMpegCore/Helpers/FFMpegHelper.cs +++ b/FFMpegCore/Helpers/FFMpegHelper.cs @@ -38,8 +38,8 @@ public static void RootExceptionCheck() public static void VerifyFFMpegExists(FFOptions ffMpegOptions) { if (_ffmpegVerified) return; - var (exitCode, _) = Instance.Finish(GlobalFFOptions.GetFFMpegBinaryPath(ffMpegOptions), "-version"); - _ffmpegVerified = exitCode == 0; + var result = Instance.Finish(GlobalFFOptions.GetFFMpegBinaryPath(ffMpegOptions), "-version"); + _ffmpegVerified = result.ExitCode == 0; if (!_ffmpegVerified) throw new FFMpegException(FFMpegExceptionType.Operation, "ffmpeg was not found on your system"); } diff --git a/FFMpegCore/Helpers/FFProbeHelper.cs b/FFMpegCore/Helpers/FFProbeHelper.cs index 4989542..f5b3472 100644 --- a/FFMpegCore/Helpers/FFProbeHelper.cs +++ b/FFMpegCore/Helpers/FFProbeHelper.cs @@ -27,8 +27,8 @@ public static void RootExceptionCheck() public static void VerifyFFProbeExists(FFOptions ffMpegOptions) { if (_ffprobeVerified) return; - var (exitCode, _) = Instance.Finish(GlobalFFOptions.GetFFProbeBinaryPath(ffMpegOptions), "-version"); - _ffprobeVerified = exitCode == 0; + var result = Instance.Finish(GlobalFFOptions.GetFFProbeBinaryPath(ffMpegOptions), "-version"); + _ffprobeVerified = result.ExitCode == 0; if (!_ffprobeVerified) throw new FFProbeException("ffprobe was not found on your system"); } diff --git a/README.md b/README.md index 2c55520..7ed60ae 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,14 @@ [![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) -[![CI](https://github.com/rosenbjerg/FFMpegCore/workflows/CI/badge.svg)](https://github.com/rosenbjerg/FFMpegCore/actions?query=workflow%3ACI) +[![CI](https://github.com/rosenbjerg/FFMpegCore/workflows/CI/badge.svg)](https://github.com/rosenbjerg/FFMpegCore/actions/workflows/ci.yml) +[![GitHub code contributors](https://img.shields.io/github/contributors/rosenbjerg/FFMpegCore)](https://github.com/rosenbjerg/FFMpegCore/graphs/contributors) A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications. Supports both synchronous and asynchronous calls # API ## FFProbe - Use FFProbe to analyze media files: ```csharp @@ -21,12 +21,12 @@ or var mediaInfo = FFProbe.Analyse(inputPath); ``` - ## FFMpeg Use FFMpeg to convert your media files. Easily build your FFMpeg arguments using the fluent argument builder: Convert input file to h264/aac scaled to 720p w/ faststart, for web playback + ```csharp FFMpegArguments .FromFileInput(inputPath) @@ -192,7 +192,7 @@ await FFMpegArguments .Configure(options => options.WorkingDirectory = "./CurrentRunWorkingDir") .Configure(options => options.TemporaryFilesFolder = "./CurrentRunTmpFolder") .ProcessAsynchronously(); - ``` +``` ### Option 2