From 1b2af5fd1f8f9347a8cda058fe3eb9ac5cace1a7 Mon Sep 17 00:00:00 2001 From: Jonas Kamsker Date: Tue, 11 Jan 2022 22:42:31 +0100 Subject: [PATCH 01/30] Using correct map_metadata dynamically --- FFMpegCore.Test/MetaDataBuilderTests.cs | 26 ++++++++++ FFMpegCore/Extend/StringExtensions.cs | 49 ++++++++++++++++++- .../FFMpeg/Arguments/IDynamicArgument.cs | 14 ++++++ .../FFMpeg/Arguments/MetaDataArgument.cs | 17 +++++-- FFMpegCore/FFMpeg/FFMpegArguments.cs | 24 +++++++-- 5 files changed, 123 insertions(+), 7 deletions(-) create mode 100644 FFMpegCore/FFMpeg/Arguments/IDynamicArgument.cs 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/Extend/StringExtensions.cs b/FFMpegCore/Extend/StringExtensions.cs index 29c8d42..d07ad1e 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 @@ -66,5 +67,51 @@ public static string Replace(this string str, Dictionary replaceLi return parsedString.ToString(); } + + /// + /// Counts the number of occurrences of the specified substring within + /// the current string. + /// + /// The current string. + /// The substring we are searching for. + /// Indicates whether or not the algorithm + /// should be aggressive in its search behavior (see Remarks). Default + /// behavior is non-aggressive. + /// This algorithm has two search modes - aggressive and + /// non-aggressive. When in aggressive search mode (aggressiveSearch = + /// true), the algorithm will try to match at every possible starting + /// character index within the string. When false, all subsequent + /// character indexes within a substring match will not be evaluated. + /// For example, if the string was 'abbbc' and we were searching for + /// the substring 'bb', then aggressive search would find 2 matches + /// with starting indexes of 1 and 2. Non aggressive search would find + /// just 1 match with starting index at 1. After the match was made, + /// the non aggressive search would attempt to make it's next match + /// starting at index 3 instead of 2. + /// The count of occurrences of the substring within the string. + public static int CountOccurrences(this string s, string substring, + bool aggressiveSearch = false) + { + // if s or substring is null or empty, substring cannot be found in s + if (string.IsNullOrEmpty(s) || string.IsNullOrEmpty(substring)) + return 0; + + // if the length of substring is greater than the length of s, + // substring cannot be found in s + if (substring.Length > s.Length) + return 0; + + int count = 0, n = 0; + while ((n = s.IndexOf(substring, n, StringComparison.InvariantCulture)) != -1) + { + if (aggressiveSearch) + n++; + else + n += substring.Length; + count++; + } + + return count; + } } } \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Arguments/IDynamicArgument.cs b/FFMpegCore/FFMpeg/Arguments/IDynamicArgument.cs new file mode 100644 index 0000000..1213630 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/IDynamicArgument.cs @@ -0,0 +1,14 @@ +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); + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs b/FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs index 7e9ffc6..c34257e 100644 --- a/FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs @@ -1,11 +1,15 @@ -using System; +using FFMpegCore.Extend; + +using System; 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 +19,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 +27,12 @@ public MetaDataArgument(string metaDataContent) public void Pre() => File.WriteAllText(_tempFileName, _metaDataContent); public void Post() => File.Delete(_tempFileName); + + public string GetText(StringBuilder context) + { + var index = context.ToString().CountOccurrences("-i"); + + return $"-i \"{_tempFileName}\" -map_metadata {index}"; + } } } diff --git a/FFMpegCore/FFMpeg/FFMpegArguments.cs b/FFMpegCore/FFMpeg/FFMpegArguments.cs index 6c9784d..edc2606 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,26 @@ 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 sb = new StringBuilder(); + + foreach (var arg in _globalArguments.Arguments.Concat(Arguments)) + { + if (sb.Length != 0) + { + sb.Append(' '); + } + sb.Append(arg is IDynamicArgument dynArg ? dynArg.GetText(sb) : arg.Text); + } + + return sb.ToString(); + } 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 +44,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); From bfd2f4a908dfc398e5a6fa0d37513cea5dc41c7e Mon Sep 17 00:00:00 2001 From: Jonas Kamsker Date: Tue, 11 Jan 2022 22:46:33 +0100 Subject: [PATCH 02/30] Fixed possible nullreference --- FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs b/FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs index c34257e..6e9f77f 100644 --- a/FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs @@ -30,7 +30,7 @@ public MetaDataArgument(string metaDataContent) public string GetText(StringBuilder context) { - var index = context.ToString().CountOccurrences("-i"); + var index = context?.ToString().CountOccurrences("-i") ?? 0; return $"-i \"{_tempFileName}\" -map_metadata {index}"; } From 2626b3796b1d37dee945dd0e9a63a29bd5d55664 Mon Sep 17 00:00:00 2001 From: Jonas Kamsker Date: Wed, 12 Jan 2022 00:15:32 +0100 Subject: [PATCH 03/30] Added support for aaxc decryption --- .../Arguments/AudibleEncryptionKeyArgument.cs | 16 +++++++ .../FFMpeg/Arguments/ID3V2VersionArgument.cs | 14 ++++++ .../FFMpeg/Arguments/MapMetadataArgument.cs | 44 +++++++++++++++++++ FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs | 5 +++ FFMpegCore/FFMpeg/FFMpegArguments.cs | 7 +++ 5 files changed, 86 insertions(+) create mode 100644 FFMpegCore/FFMpeg/Arguments/AudibleEncryptionKeyArgument.cs create mode 100644 FFMpegCore/FFMpeg/Arguments/ID3V2VersionArgument.cs create mode 100644 FFMpegCore/FFMpeg/Arguments/MapMetadataArgument.cs diff --git a/FFMpegCore/FFMpeg/Arguments/AudibleEncryptionKeyArgument.cs b/FFMpegCore/FFMpeg/Arguments/AudibleEncryptionKeyArgument.cs new file mode 100644 index 0000000..9f1a325 --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/AudibleEncryptionKeyArgument.cs @@ -0,0 +1,16 @@ +namespace FFMpegCore.Arguments +{ + public class AudibleEncryptionKeyArgument : IArgument + { + private readonly string _key; + private readonly string _iv; + + public AudibleEncryptionKeyArgument(string key, string iv) + { + _key = key; + _iv = iv; + } + + public string Text => $"-audible_key {_key} -audible_iv {_iv}"; + } +} 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/MapMetadataArgument.cs b/FFMpegCore/FFMpeg/Arguments/MapMetadataArgument.cs new file mode 100644 index 0000000..01b661c --- /dev/null +++ b/FFMpegCore/FFMpeg/Arguments/MapMetadataArgument.cs @@ -0,0 +1,44 @@ +using FFMpegCore.Extend; + +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace FFMpegCore.Arguments +{ + public class MapMetadataArgument : IInputArgument, IDynamicArgument + { + private readonly int? _inputIndex; + + /// + /// Null means it takes the last input used befroe this argument + /// + /// + public MapMetadataArgument(int? inputIndex = null) + { + _inputIndex = inputIndex; + } + + public string Text => GetText(null); + + public string GetText(StringBuilder context) + { + var index = _inputIndex ?? context?.ToString().CountOccurrences("-i") -1 ?? 0; + return $"-map_metadata {index}"; + } + + + public Task During(CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + public void Post() + { + } + + public void Pre() + { + } + } +} diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs index ca6628a..b4f5111 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,10 @@ 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 WithTagVersion(int id3v2Version = 3) => WithArgument(new ID3V2VersionArgument(id3v2Version)); + + public FFMpegArgumentOptions WithArgument(IArgument argument) { Arguments.Add(argument); diff --git a/FFMpegCore/FFMpeg/FFMpegArguments.cs b/FFMpegCore/FFMpeg/FFMpegArguments.cs index edc2606..a628867 100644 --- a/FFMpegCore/FFMpeg/FFMpegArguments.cs +++ b/FFMpegCore/FFMpeg/FFMpegArguments.cs @@ -60,6 +60,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(); From c05337562cb98eb1c08a68147c2ed88e230c8203 Mon Sep 17 00:00:00 2001 From: Jonas Kamsker <11245306+JKamsker@users.noreply.github.com> Date: Wed, 12 Jan 2022 00:31:47 +0100 Subject: [PATCH 04/30] Fixed GetText to pleasure the unit test --- FFMpegCore/FFMpeg/FFMpegArguments.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/FFMpegCore/FFMpeg/FFMpegArguments.cs b/FFMpegCore/FFMpeg/FFMpegArguments.cs index edc2606..4546dbe 100644 --- a/FFMpegCore/FFMpeg/FFMpegArguments.cs +++ b/FFMpegCore/FFMpeg/FFMpegArguments.cs @@ -23,13 +23,19 @@ private FFMpegArguments() { } private string GetText() { var sb = new StringBuilder(); + var appendSpace = false; foreach (var arg in _globalArguments.Arguments.Concat(Arguments)) { - if (sb.Length != 0) + if (appendSpace) { sb.Append(' '); } + else + { + appendSpace = true; + } + sb.Append(arg is IDynamicArgument dynArg ? dynArg.GetText(sb) : arg.Text); } @@ -99,4 +105,4 @@ internal void Post() argument.Post(); } } -} \ No newline at end of file +} From d6bc6c92a5a8a3457b77618832f8bde83429ad4b Mon Sep 17 00:00:00 2001 From: Jonas Kamsker Date: Wed, 12 Jan 2022 00:34:52 +0100 Subject: [PATCH 05/30] Added unit tests --- FFMpegCore.Test/ArgumentBuilderTest.cs | 15 +++++++++++++-- FFMpegCore.Test/FFMpegArgumentProcessorTest.cs | 7 +++++++ 2 files changed, 20 insertions(+), 2 deletions(-) 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..c0a565e 100644 --- a/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs +++ b/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs @@ -94,5 +94,12 @@ 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"); + } } } \ No newline at end of file From 07470ec1538805737ede92a357580d56c5c8efd6 Mon Sep 17 00:00:00 2001 From: Jonas Kamsker <11245306+JKamsker@users.noreply.github.com> Date: Wed, 12 Jan 2022 01:01:40 +0100 Subject: [PATCH 06/30] Update FFProbeAnalysis.cs --- FFMpegCore/FFProbe/FFProbeAnalysis.cs | 3 +++ 1 file changed, 3 insertions(+) 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 From 3324cbb528834ef8bf36485c51f47c70635127de Mon Sep 17 00:00:00 2001 From: Jonas Kamsker <11245306+JKamsker@users.noreply.github.com> Date: Wed, 12 Jan 2022 01:03:51 +0100 Subject: [PATCH 07/30] Added errordata to analysis --- FFMpegCore/FFProbe/FFProbe.cs | 1 + FFMpegCore/FFProbe/IMediaAnalysis.cs | 1 + FFMpegCore/FFProbe/MediaAnalysis.cs | 4 +++- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/FFMpegCore/FFProbe/FFProbe.cs b/FFMpegCore/FFProbe/FFProbe.cs index 36f050c..d0f056e 100644 --- a/FFMpegCore/FFProbe/FFProbe.cs +++ b/FFMpegCore/FFProbe/FFProbe.cs @@ -163,6 +163,7 @@ 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) 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..bf16544 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) @@ -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 From 1d2c0f760b56e48b3313540254ad5e3c4a1fac95 Mon Sep 17 00:00:00 2001 From: Notheisz57 Date: Sat, 29 Jan 2022 19:29:45 -0800 Subject: [PATCH 08/30] Update FFProbe.cs Pass FFOptions argument in call to GlobalFFOptions.GetFFProbeBinaryPath --- FFMpegCore/FFProbe/FFProbe.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FFMpegCore/FFProbe/FFProbe.cs b/FFMpegCore/FFProbe/FFProbe.cs index 36f050c..4418371 100644 --- a/FFMpegCore/FFProbe/FFProbe.cs +++ b/FFMpegCore/FFProbe/FFProbe.cs @@ -201,7 +201,7 @@ private static Instance PrepareInstance(string arguments, int outputCapacity, FF { 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, From 7f8bd2405860b4e8d53ec276fc48628dabd78649 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Sat, 12 Mar 2022 19:06:46 +0100 Subject: [PATCH 09/30] Init --- FFMpegCore.Test/FFProbeTests.cs | 7 - FFMpegCore.Test/VideoTest.cs | 2 +- FFMpegCore/FFMpeg/FFMpeg.cs | 32 ++-- FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs | 119 +++++++-------- FFMpegCore/FFMpegCore.csproj | 4 +- FFMpegCore/FFProbe/FFProbe.cs | 139 +++++++++--------- .../FFProbe/ProcessArgumentsExtensions.cs | 20 +++ FFMpegCore/Helpers/FFMpegHelper.cs | 4 +- FFMpegCore/Helpers/FFProbeHelper.cs | 4 +- 9 files changed, 174 insertions(+), 157 deletions(-) create mode 100644 FFMpegCore/FFProbe/ProcessArgumentsExtensions.cs 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/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 0f806d6..56579e9 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -544,7 +544,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); diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index 6f0ede3..d227a2e 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 { @@ -417,15 +418,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 +464,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 +475,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 +548,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/FFMpegArgumentProcessor.cs b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs index fdbdcc8..34ccc59 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,43 +86,47 @@ 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); + processArguments.Exited += delegate { cancellationTokenSource.Cancel(); }; - 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) { - if (!HandleException(throwOnError, e, instance.ErrorData)) return false; - } - finally - { - CancelEvent -= OnCancelEvent; + if (!HandleException(throwOnError, e, processResult?.ErrorData ?? Array.Empty())) return false; } - 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 (Exception e) + { + if (!HandleException(throwOnError, e, processResult?.ErrorData ?? Array.Empty())) return false; + } + + 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(); void OnCancelEvent(object sender, int timeout) { instance.SendInput("q"); @@ -124,41 +134,26 @@ void OnCancelEvent(object sender, int timeout) 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); + + 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 +179,7 @@ internal FFOptions GetConfiguredOptions(FFOptions? ffOptions) return options; } - private Instance PrepareInstance(FFOptions ffOptions, + private ProcessArguments PrepareProcessArguments(FFOptions ffOptions, out CancellationTokenSource cancellationTokenSource) { FFMpegHelper.RootExceptionCheck(); @@ -197,17 +192,25 @@ 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 void ErrorData(object sender, string msg) + { + _onError?.Invoke(msg); } - private static bool HandleException(bool throwOnError, Exception e, IReadOnlyList errorData) + private static bool HandleException(bool throwOnError, Exception e, IEnumerable errorData) { if (!throwOnError) return false; @@ -215,12 +218,12 @@ private static bool HandleException(bool throwOnError, Exception e, IReadOnlyLis throw new FFMpegException(FFMpegExceptionType.Process, "Exception thrown during processing", e, string.Join("\n", errorData)); } - 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/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/FFProbe.cs b/FFMpegCore/FFProbe/FFProbe.cs index 36f050c..afa7c82 100644 --- a/FFMpegCore/FFProbe/FFProbe.cs +++ b/FFMpegCore/FFProbe/FFProbe.cs @@ -13,61 +13,61 @@ 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}'"); - 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(); + if (result.ExitCode != 0) + throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({result.ExitCode} - {string.Join("\n", result.ErrorData)})", null, string.Join("\n", result.ErrorData)); - 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}'"); - 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(); + if (result.ExitCode != 0) + throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({result.ExitCode} - {string.Join("\n", result.ErrorData)})", null, string.Join("\n", result.ErrorData)); - 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}'"); - 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(); + if (result.ExitCode != 0) + throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({result.ExitCode} - {string.Join("\n", result.ErrorData)})", null, string.Join("\n", result.ErrorData)); - 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(); + if (result.ExitCode != 0) + throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({result.ExitCode} - {string.Join("\n", result.ErrorData)})", null, string.Join("\n", result.ErrorData)); - 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,62 +77,62 @@ 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(); + if (result.ExitCode != 0) + throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({result.ExitCode} - {string.Join("\n", result.ErrorData)})", null, string.Join("\n", result.ErrorData)); - 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) { if (!File.Exists(filePath)) throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{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().ConfigureAwait(false); + if (result.ExitCode != 0) + throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({result.ExitCode} - {string.Join("\n", result.ErrorData)})", null, string.Join("\n", result.ErrorData)); - 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) { if (!File.Exists(filePath)) throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{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().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) { if (!File.Exists(filePath)) throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{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().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) { - 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().ConfigureAwait(false); + if (result.ExitCode != 0) + throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({result.ExitCode} - {string.Join("\n", result.ErrorData)})", null, string.Join("\n", result.ErrorData)); - 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) { 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 { await pipeArgument.During().ConfigureAwait(false); @@ -144,15 +144,15 @@ 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); + if (result.ExitCode != 0) + throw new FFProbeProcessException($"ffprobe exited with non-zero exit-code ({result.ExitCode} - {string.Join("\n", result.ErrorData)})", result.ErrorData); 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 @@ -165,7 +165,7 @@ private static IMediaAnalysis ParseOutput(Instance instance) 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 @@ -177,7 +177,7 @@ private static FFProbeFrames ParseFramesOutput(Instance instance) 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 @@ -190,14 +190,14 @@ private static FFProbePackets ParsePacketsOutput(Instance instance) } - 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 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); @@ -207,8 +207,7 @@ private static Instance PrepareInstance(string arguments, int outputCapacity, FF StandardErrorEncoding = ffOptions.Encoding, WorkingDirectory = ffOptions.WorkingDirectory }; - var instance = new Instance(startInfo) { DataBufferCapacity = outputCapacity }; - return instance; + return new ProcessArguments(startInfo); } } } 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/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"); } From 4e977c664708c1ac82105e05f61093edc066383a Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Sun, 13 Mar 2022 11:43:48 +0100 Subject: [PATCH 10/30] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2c55520..df5679a 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ [![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 From 5b0d3d21c9946fb8095559faf3a4ef139115a2ef Mon Sep 17 00:00:00 2001 From: Jonas Kamsker <11245306+JKamsker@users.noreply.github.com> Date: Sun, 20 Mar 2022 19:31:15 +0100 Subject: [PATCH 11/30] Applied reqested changes --- FFMpegCore/Extend/StringExtensions.cs | 5 +-- .../FFMpeg/Arguments/IDynamicArgument.cs | 6 ++-- .../FFMpeg/Arguments/MetaDataArgument.cs | 10 ++++-- FFMpegCore/FFMpeg/FFMpegArguments.cs | 33 ++++++++++--------- 4 files changed, 31 insertions(+), 23 deletions(-) diff --git a/FFMpegCore/Extend/StringExtensions.cs b/FFMpegCore/Extend/StringExtensions.cs index d07ad1e..35f2e84 100644 --- a/FFMpegCore/Extend/StringExtensions.cs +++ b/FFMpegCore/Extend/StringExtensions.cs @@ -93,12 +93,9 @@ public static int CountOccurrences(this string s, string substring, bool aggressiveSearch = false) { // if s or substring is null or empty, substring cannot be found in s - if (string.IsNullOrEmpty(s) || string.IsNullOrEmpty(substring)) - return 0; - // if the length of substring is greater than the length of s, // substring cannot be found in s - if (substring.Length > s.Length) + if (string.IsNullOrEmpty(s) || string.IsNullOrEmpty(substring) || substring.Length > s.Length) return 0; int count = 0, n = 0; diff --git a/FFMpegCore/FFMpeg/Arguments/IDynamicArgument.cs b/FFMpegCore/FFMpeg/Arguments/IDynamicArgument.cs index 1213630..36a504e 100644 --- a/FFMpegCore/FFMpeg/Arguments/IDynamicArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/IDynamicArgument.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Collections.Generic; +using System.Text; namespace FFMpegCore.Arguments { @@ -9,6 +10,7 @@ public interface IDynamicArgument /// /// /// - public string GetText(StringBuilder context); + //public string GetText(StringBuilder context); + public string GetText(IEnumerable context); } } \ No newline at end of file diff --git a/FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs b/FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs index c34257e..89bb1fe 100644 --- a/FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/MetaDataArgument.cs @@ -1,6 +1,7 @@ using FFMpegCore.Extend; using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; @@ -28,9 +29,14 @@ public MetaDataArgument(string metaDataContent) public void Post() => File.Delete(_tempFileName); - public string GetText(StringBuilder context) + public string GetText(IEnumerable? arguments) { - var index = context.ToString().CountOccurrences("-i"); + arguments ??= Enumerable.Empty(); + + var index = arguments + .TakeWhile(x => x != this) + .OfType() + .Count(); return $"-i \"{_tempFileName}\" -map_metadata {index}"; } diff --git a/FFMpegCore/FFMpeg/FFMpegArguments.cs b/FFMpegCore/FFMpeg/FFMpegArguments.cs index 4546dbe..e7a5940 100644 --- a/FFMpegCore/FFMpeg/FFMpegArguments.cs +++ b/FFMpegCore/FFMpeg/FFMpegArguments.cs @@ -22,24 +22,27 @@ private FFMpegArguments() { } private string GetText() { - var sb = new StringBuilder(); - var appendSpace = false; + //var sb = new StringBuilder(); + //var appendSpace = false; - foreach (var arg in _globalArguments.Arguments.Concat(Arguments)) - { - if (appendSpace) - { - sb.Append(' '); - } - else - { - appendSpace = true; - } + //foreach (var arg in _globalArguments.Arguments.Concat(Arguments)) + //{ + // if (appendSpace) + // { + // sb.Append(' '); + // } + // else + // { + // appendSpace = true; + // } - sb.Append(arg is IDynamicArgument dynArg ? dynArg.GetText(sb) : arg.Text); - } + // sb.Append(arg is IDynamicArgument dynArg ? dynArg.GetText(sb) : arg.Text); + //} - return sb.ToString(); + //return sb.ToString(); + + 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); From 373c604a11b4050846d88a15f57b919e16dd7e1e Mon Sep 17 00:00:00 2001 From: Jonas Kamsker <11245306+JKamsker@users.noreply.github.com> Date: Sun, 20 Mar 2022 19:32:48 +0100 Subject: [PATCH 12/30] Removed string count extension --- FFMpegCore/Extend/StringExtensions.cs | 43 --------------------------- 1 file changed, 43 deletions(-) diff --git a/FFMpegCore/Extend/StringExtensions.cs b/FFMpegCore/Extend/StringExtensions.cs index 35f2e84..7b02089 100644 --- a/FFMpegCore/Extend/StringExtensions.cs +++ b/FFMpegCore/Extend/StringExtensions.cs @@ -67,48 +67,5 @@ public static string Replace(this string str, Dictionary replaceLi return parsedString.ToString(); } - - /// - /// Counts the number of occurrences of the specified substring within - /// the current string. - /// - /// The current string. - /// The substring we are searching for. - /// Indicates whether or not the algorithm - /// should be aggressive in its search behavior (see Remarks). Default - /// behavior is non-aggressive. - /// This algorithm has two search modes - aggressive and - /// non-aggressive. When in aggressive search mode (aggressiveSearch = - /// true), the algorithm will try to match at every possible starting - /// character index within the string. When false, all subsequent - /// character indexes within a substring match will not be evaluated. - /// For example, if the string was 'abbbc' and we were searching for - /// the substring 'bb', then aggressive search would find 2 matches - /// with starting indexes of 1 and 2. Non aggressive search would find - /// just 1 match with starting index at 1. After the match was made, - /// the non aggressive search would attempt to make it's next match - /// starting at index 3 instead of 2. - /// The count of occurrences of the substring within the string. - public static int CountOccurrences(this string s, string substring, - bool aggressiveSearch = false) - { - // if s or substring is null or empty, substring cannot be found in s - // if the length of substring is greater than the length of s, - // substring cannot be found in s - if (string.IsNullOrEmpty(s) || string.IsNullOrEmpty(substring) || substring.Length > s.Length) - return 0; - - int count = 0, n = 0; - while ((n = s.IndexOf(substring, n, StringComparison.InvariantCulture)) != -1) - { - if (aggressiveSearch) - n++; - else - n += substring.Length; - count++; - } - - return count; - } } } \ No newline at end of file From 353bbff1b883ebeaf2412977ae581bb548585e68 Mon Sep 17 00:00:00 2001 From: Jonas Kamsker <11245306+JKamsker@users.noreply.github.com> Date: Sun, 20 Mar 2022 19:34:01 +0100 Subject: [PATCH 13/30] Removed comment --- FFMpegCore/FFMpeg/FFMpegArguments.cs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/FFMpegCore/FFMpeg/FFMpegArguments.cs b/FFMpegCore/FFMpeg/FFMpegArguments.cs index e7a5940..a93f1bd 100644 --- a/FFMpegCore/FFMpeg/FFMpegArguments.cs +++ b/FFMpegCore/FFMpeg/FFMpegArguments.cs @@ -22,25 +22,6 @@ private FFMpegArguments() { } private string GetText() { - //var sb = new StringBuilder(); - //var appendSpace = false; - - //foreach (var arg in _globalArguments.Arguments.Concat(Arguments)) - //{ - // if (appendSpace) - // { - // sb.Append(' '); - // } - // else - // { - // appendSpace = true; - // } - - // sb.Append(arg is IDynamicArgument dynArg ? dynArg.GetText(sb) : arg.Text); - //} - - //return sb.ToString(); - var allArguments = _globalArguments.Arguments.Concat(Arguments).ToArray(); return string.Join(" ", allArguments.Select(arg => arg is IDynamicArgument dynArg ? dynArg.GetText(allArguments) : arg.Text)); } From f42faa4f264b8ee84016c1386400a05545c5be1b Mon Sep 17 00:00:00 2001 From: Jonas Kamsker <11245306+JKamsker@users.noreply.github.com> Date: Sun, 20 Mar 2022 19:56:17 +0100 Subject: [PATCH 14/30] Fixed merge & implemented aax mode --- .../FFMpegArgumentProcessorTest.cs | 8 +++++ .../Arguments/AudibleEncryptionKeyArgument.cs | 14 +++++++- .../FFMpeg/Arguments/MapMetadataArgument.cs | 32 +++++++++++++++---- FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs | 1 + 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs b/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs index c0a565e..6e30999 100644 --- a/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs +++ b/FFMpegCore.Test/FFMpegArgumentProcessorTest.cs @@ -101,5 +101,13 @@ 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/FFMpeg/Arguments/AudibleEncryptionKeyArgument.cs b/FFMpegCore/FFMpeg/Arguments/AudibleEncryptionKeyArgument.cs index 9f1a325..0f514dc 100644 --- a/FFMpegCore/FFMpeg/Arguments/AudibleEncryptionKeyArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/AudibleEncryptionKeyArgument.cs @@ -2,15 +2,27 @@ { 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 => $"-audible_key {_key} -audible_iv {_iv}"; + public string Text => _aaxcMode ? $"-audible_key {_key} -audible_iv {_iv}" : $"-activation_bytes {_activationBytes}"; } } diff --git a/FFMpegCore/FFMpeg/Arguments/MapMetadataArgument.cs b/FFMpegCore/FFMpeg/Arguments/MapMetadataArgument.cs index 01b661c..afec731 100644 --- a/FFMpegCore/FFMpeg/Arguments/MapMetadataArgument.cs +++ b/FFMpegCore/FFMpeg/Arguments/MapMetadataArgument.cs @@ -1,5 +1,8 @@ using FFMpegCore.Extend; +using System; +using System.Collections.Generic; +using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -10,8 +13,10 @@ public class MapMetadataArgument : IInputArgument, IDynamicArgument { private readonly int? _inputIndex; + public string Text => GetText(null); + /// - /// Null means it takes the last input used befroe this argument + /// Null means it takes the last input used before this argument /// /// public MapMetadataArgument(int? inputIndex = null) @@ -19,15 +24,28 @@ public MapMetadataArgument(int? inputIndex = null) _inputIndex = inputIndex; } - public string Text => GetText(null); - - public string GetText(StringBuilder context) + public string GetText(IEnumerable? arguments) { - var index = _inputIndex ?? context?.ToString().CountOccurrences("-i") -1 ?? 0; + 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; @@ -40,5 +58,7 @@ public void Post() public void Pre() { } + + } } diff --git a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs index b4f5111..7b3da7a 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentOptions.cs @@ -68,6 +68,7 @@ public FFMpegArgumentOptions WithAudioFilters(Action audioFi 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)); From 05839825e0ec9edb867dfb4279975529bba5e3aa Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Thu, 24 Mar 2022 20:24:22 +0100 Subject: [PATCH 15/30] Wrap image size check in using block #304 --- FFMpegCore/FFMpeg/FFMpeg.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index d227a2e..4e7d47a 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -247,7 +247,10 @@ 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 From 2133d310217f760678fa28c8f5b8409b8e5d9c21 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Thu, 24 Mar 2022 20:27:09 +0100 Subject: [PATCH 16/30] Expose avg_frame_rate as AverageFrameRate on VideoStream #300 --- FFMpegCore/FFProbe/FFProbeAnalysis.cs | 3 +++ FFMpegCore/FFProbe/MediaAnalysis.cs | 1 + FFMpegCore/FFProbe/VideoStream.cs | 1 + 3 files changed, 5 insertions(+) diff --git a/FFMpegCore/FFProbe/FFProbeAnalysis.cs b/FFMpegCore/FFProbe/FFProbeAnalysis.cs index 2177307..9967508 100644 --- a/FFMpegCore/FFProbe/FFProbeAnalysis.cs +++ b/FFMpegCore/FFProbe/FFProbeAnalysis.cs @@ -64,6 +64,9 @@ public class FFProbeStream : ITagsContainer, IDispositionContainer [JsonPropertyName("r_frame_rate")] public string FrameRate { get; set; } = null!; + + [JsonPropertyName("avg_frame_rate")] + public string AverageFrameRate { get; set; } = null!; [JsonPropertyName("pix_fmt")] public string PixelFormat { get; set; } = null!; diff --git a/FFMpegCore/FFProbe/MediaAnalysis.cs b/FFMpegCore/FFProbe/MediaAnalysis.cs index a2db068..72fc3a9 100644 --- a/FFMpegCore/FFProbe/MediaAnalysis.cs +++ b/FFMpegCore/FFProbe/MediaAnalysis.cs @@ -61,6 +61,7 @@ private VideoStream ParseVideoStream(FFProbeStream stream) DisplayAspectRatio = MediaAnalysisUtils.ParseRatioInt(stream.DisplayAspectRatio, ':'), Duration = MediaAnalysisUtils.ParseDuration(stream), FrameRate = MediaAnalysisUtils.DivideRatio(MediaAnalysisUtils.ParseRatioDouble(stream.FrameRate, '/')), + AverageFrameRate = MediaAnalysisUtils.DivideRatio(MediaAnalysisUtils.ParseRatioDouble(stream.AverageFrameRate, '/')), Height = stream.Height ?? 0, Width = stream.Width ?? 0, Profile = stream.Profile, diff --git a/FFMpegCore/FFProbe/VideoStream.cs b/FFMpegCore/FFProbe/VideoStream.cs index 0bcfc09..8738212 100644 --- a/FFMpegCore/FFProbe/VideoStream.cs +++ b/FFMpegCore/FFProbe/VideoStream.cs @@ -13,6 +13,7 @@ public class VideoStream : MediaStream public double FrameRate { get; internal set; } public string PixelFormat { get; internal set; } = null!; public int Rotation { get; set; } + public double AverageFrameRate { get; set; } public PixelFormat GetPixelFormatInfo() => FFMpeg.GetPixelFormat(PixelFormat); } From da251e99ad7279085d9ea2d44642726ad2391c28 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Thu, 24 Mar 2022 20:34:25 +0100 Subject: [PATCH 17/30] Support cancellationtoken on async ffprobe calls #299 --- FFMpegCore/FFProbe/FFProbe.cs | 83 +++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/FFMpegCore/FFProbe/FFProbe.cs b/FFMpegCore/FFProbe/FFProbe.cs index 75f0df1..cfd719e 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; @@ -15,38 +16,33 @@ public static class FFProbe { 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); var processArguments = PrepareStreamAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current); var result = processArguments.StartAndWaitForExit(); - if (result.ExitCode != 0) - throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({result.ExitCode} - {string.Join("\n", result.ErrorData)})", null, string.Join("\n", result.ErrorData)); + ThrowIfExitCodeNotZero(result); return ParseOutput(result); } + 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); var instance = PrepareFrameAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current); var result = instance.StartAndWaitForExit(); - if (result.ExitCode != 0) - throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({result.ExitCode} - {string.Join("\n", result.ErrorData)})", null, string.Join("\n", result.ErrorData)); + ThrowIfExitCodeNotZero(result); return ParseFramesOutput(result); } 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); var instance = PreparePacketAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current); var result = instance.StartAndWaitForExit(); - if (result.ExitCode != 0) - throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({result.ExitCode} - {string.Join("\n", result.ErrorData)})", null, string.Join("\n", result.ErrorData)); + ThrowIfExitCodeNotZero(result); return ParsePacketsOutput(result); } @@ -55,8 +51,7 @@ public static IMediaAnalysis Analyse(Uri uri, FFOptions? ffOptions = null) { var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current); var result = instance.StartAndWaitForExit(); - if (result.ExitCode != 0) - throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({result.ExitCode} - {string.Join("\n", result.ErrorData)})", null, string.Join("\n", result.ErrorData)); + ThrowIfExitCodeNotZero(result); return ParseOutput(result); } @@ -78,64 +73,59 @@ public static IMediaAnalysis Analyse(Stream stream, FFOptions? ffOptions = null) pipeArgument.Post(); } var result = task.ConfigureAwait(false).GetAwaiter().GetResult(); - if (result.ExitCode != 0) - throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({result.ExitCode} - {string.Join("\n", result.ErrorData)})", null, string.Join("\n", result.ErrorData)); + ThrowIfExitCodeNotZero(result); return ParseOutput(result); } - public static async Task AnalyseAsync(string filePath, 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); var instance = PrepareStreamAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current); - var result = await instance.StartAndWaitForExitAsync().ConfigureAwait(false); - if (result.ExitCode != 0) - throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({result.ExitCode} - {string.Join("\n", result.ErrorData)})", null, string.Join("\n", result.ErrorData)); + var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); + ThrowIfExitCodeNotZero(result); return ParseOutput(result); } - public static async Task GetFramesAsync(string filePath, 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); var instance = PrepareFrameAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current); - var result = await instance.StartAndWaitForExitAsync().ConfigureAwait(false); + var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); return ParseFramesOutput(result); } - public static async Task GetPacketsAsync(string filePath, 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); var instance = PreparePacketAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current); - var result = await instance.StartAndWaitForExitAsync().ConfigureAwait(false); + var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); return ParsePacketsOutput(result); } - public static async Task AnalyseAsync(Uri uri, FFOptions? ffOptions = null) + public static async Task AnalyseAsync(Uri uri, FFOptions? ffOptions = null, CancellationToken cancellationToken = default) { var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current); - var result = await instance.StartAndWaitForExitAsync().ConfigureAwait(false); - if (result.ExitCode != 0) - throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({result.ExitCode} - {string.Join("\n", result.ErrorData)})", null, string.Join("\n", result.ErrorData)); + var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false); + ThrowIfExitCodeNotZero(result); return ParseOutput(result); } - public static async Task AnalyseAsync(Stream stream, 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); var instance = PrepareStreamAnalysisInstance(pipeArgument.PipePath, ffOptions ?? GlobalFFOptions.Current); pipeArgument.Pre(); - var task = instance.StartAndWaitForExitAsync(); + var task = instance.StartAndWaitForExitAsync(cancellationToken); try { - await pipeArgument.During().ConfigureAwait(false); + await pipeArgument.During(cancellationToken).ConfigureAwait(false); } catch(IOException) { @@ -145,8 +135,7 @@ public static async Task AnalyseAsync(Stream stream, FFOptions? pipeArgument.Post(); } var result = await task.ConfigureAwait(false); - if (result.ExitCode != 0) - throw new FFProbeProcessException($"ffprobe exited with non-zero exit-code ({result.ExitCode} - {string.Join("\n", result.ErrorData)})", result.ErrorData); + ThrowIfExitCodeNotZero(result); pipeArgument.Post(); return ParseOutput(result); @@ -189,6 +178,22 @@ private static FFProbePackets ParsePacketsOutput(IProcessResult instance) return ffprobeAnalysis; } + private static void ThrowIfInputFileDoesNotExist(string filePath) + { + if (!File.Exists(filePath)) + { + throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'"); + } + } + + private static void ThrowIfExitCodeNotZero(IProcessResult result) + { + if (result.ExitCode != 0) + { + var message = $"ffprobe exited with non-zero exit-code ({result.ExitCode} - {string.Join("\n", result.ErrorData)})"; + throw new FFMpegException(FFMpegExceptionType.Process, message, null, string.Join("\n", result.ErrorData)); + } + } private static ProcessArguments PrepareStreamAnalysisInstance(string filePath, FFOptions ffOptions) => PrepareInstance($"-loglevel error -print_format json -show_format -sexagesimal -show_streams \"{filePath}\"", ffOptions); From fc86a64b9e75637652480f200a5e92dd5cfd42d0 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Thu, 24 Mar 2022 20:37:27 +0100 Subject: [PATCH 18/30] Make properties on MediaStream classes for deserialization #296 --- FFMpegCore/FFProbe/AudioStream.cs | 8 ++++---- FFMpegCore/FFProbe/FFProbe.cs | 4 ++-- FFMpegCore/FFProbe/MediaStream.cs | 18 +++++++++--------- FFMpegCore/FFProbe/VideoStream.cs | 16 ++++++++-------- 4 files changed, 23 insertions(+), 23 deletions(-) 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 cfd719e..15ad171 100644 --- a/FFMpegCore/FFProbe/FFProbe.cs +++ b/FFMpegCore/FFProbe/FFProbe.cs @@ -163,7 +163,7 @@ private static FFProbeFrames ParseFramesOutput(IProcessResult instance) NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString | System.Text.Json.Serialization.JsonNumberHandling.WriteAsString }) ; - return ffprobeAnalysis; + return ffprobeAnalysis!; } private static FFProbePackets ParsePacketsOutput(IProcessResult instance) @@ -175,7 +175,7 @@ private static FFProbePackets ParsePacketsOutput(IProcessResult instance) NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString | System.Text.Json.Serialization.JsonNumberHandling.WriteAsString }) ; - return ffprobeAnalysis; + return ffprobeAnalysis!; } private static void ThrowIfInputFileDoesNotExist(string filePath) 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/VideoStream.cs b/FFMpegCore/FFProbe/VideoStream.cs index 8738212..a07cdd9 100644 --- a/FFMpegCore/FFProbe/VideoStream.cs +++ b/FFMpegCore/FFProbe/VideoStream.cs @@ -4,14 +4,14 @@ 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; } From 90155efe75c4bf704a8e7cf16de6e50620b39250 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Thu, 24 Mar 2022 20:45:49 +0100 Subject: [PATCH 19/30] Make Tags and Disposition dictionaries case-insensivite #295 --- FFMpegCore/FFProbe/MediaAnalysis.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/FFMpegCore/FFProbe/MediaAnalysis.cs b/FFMpegCore/FFProbe/MediaAnalysis.cs index 72fc3a9..42cc5e5 100644 --- a/FFMpegCore/FFProbe/MediaAnalysis.cs +++ b/FFMpegCore/FFProbe/MediaAnalysis.cs @@ -25,7 +25,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(), }; } @@ -69,7 +69,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(), }; } @@ -90,7 +90,7 @@ private AudioStream ParseAudioStream(FFProbeStream stream) Profile = stream.Profile, Language = stream.GetLanguage(), Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition), - Tags = stream.Tags, + Tags = stream.Tags.ToCaseInsensitive(), }; } @@ -105,15 +105,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); + } public static double DivideRatio((double, double) ratio) => ratio.Item1 / ratio.Item2; public static (int, int) ParseRatioInt(string input, char separator) @@ -184,7 +189,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) { From 787804c3a288578e57c501eabeaaf767269248eb Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Thu, 24 Mar 2022 20:46:02 +0100 Subject: [PATCH 20/30] Clean warnings --- FFMpegCore/FFProbe/FrameAnalysis.cs | 18 +++++++++--------- FFMpegCore/FFProbe/PacketAnalysis.cs | 12 ++++++------ 2 files changed, 15 insertions(+), 15 deletions(-) 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/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!; } } From c817381139b247842cd44f3524c77a8981dc6626 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Thu, 24 Mar 2022 21:14:00 +0100 Subject: [PATCH 21/30] Remove duplicated property --- FFMpegCore/FFProbe/FFProbeAnalysis.cs | 3 --- FFMpegCore/FFProbe/MediaAnalysis.cs | 1 - 2 files changed, 4 deletions(-) diff --git a/FFMpegCore/FFProbe/FFProbeAnalysis.cs b/FFMpegCore/FFProbe/FFProbeAnalysis.cs index 9967508..2177307 100644 --- a/FFMpegCore/FFProbe/FFProbeAnalysis.cs +++ b/FFMpegCore/FFProbe/FFProbeAnalysis.cs @@ -64,9 +64,6 @@ public class FFProbeStream : ITagsContainer, IDispositionContainer [JsonPropertyName("r_frame_rate")] public string FrameRate { get; set; } = null!; - - [JsonPropertyName("avg_frame_rate")] - public string AverageFrameRate { get; set; } = null!; [JsonPropertyName("pix_fmt")] public string PixelFormat { get; set; } = null!; diff --git a/FFMpegCore/FFProbe/MediaAnalysis.cs b/FFMpegCore/FFProbe/MediaAnalysis.cs index 42cc5e5..3ecdacd 100644 --- a/FFMpegCore/FFProbe/MediaAnalysis.cs +++ b/FFMpegCore/FFProbe/MediaAnalysis.cs @@ -61,7 +61,6 @@ private VideoStream ParseVideoStream(FFProbeStream stream) DisplayAspectRatio = MediaAnalysisUtils.ParseRatioInt(stream.DisplayAspectRatio, ':'), Duration = MediaAnalysisUtils.ParseDuration(stream), FrameRate = MediaAnalysisUtils.DivideRatio(MediaAnalysisUtils.ParseRatioDouble(stream.FrameRate, '/')), - AverageFrameRate = MediaAnalysisUtils.DivideRatio(MediaAnalysisUtils.ParseRatioDouble(stream.AverageFrameRate, '/')), Height = stream.Height ?? 0, Width = stream.Width ?? 0, Profile = stream.Profile, From 1c851dc3ff7085d6c09f94f477ed5b05e1539835 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Thu, 24 Mar 2022 21:15:28 +0100 Subject: [PATCH 22/30] Handle null dictionaries --- FFMpegCore/FFProbe/MediaAnalysis.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FFMpegCore/FFProbe/MediaAnalysis.cs b/FFMpegCore/FFProbe/MediaAnalysis.cs index 3ecdacd..b5f9c34 100644 --- a/FFMpegCore/FFProbe/MediaAnalysis.cs +++ b/FFMpegCore/FFProbe/MediaAnalysis.cs @@ -114,9 +114,9 @@ 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) + internal static Dictionary? ToCaseInsensitive(this Dictionary? dictionary) { - return dictionary.ToDictionary(tag => tag.Key, tag => tag.Value, StringComparer.OrdinalIgnoreCase); + return dictionary?.ToDictionary(tag => tag.Key, tag => tag.Value, StringComparer.OrdinalIgnoreCase) ?? new Dictionary(); } public static double DivideRatio((double, double) ratio) => ratio.Item1 / ratio.Item2; From 5dcbcfeaaf46b190c9ebafa9b121e11a757e426b Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Thu, 24 Mar 2022 21:19:27 +0100 Subject: [PATCH 23/30] Use .NET 6 in pipelines --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6946920..a68bbc6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,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 From b33f9a3c4b7e0a21f3125aa9137c0926cb717a25 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Thu, 24 Mar 2022 21:22:56 +0100 Subject: [PATCH 24/30] Bump .NET version in Test and Examples projects --- FFMpegCore.Examples/FFMpegCore.Examples.csproj | 2 +- FFMpegCore.Test/FFMpegCore.Test.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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 From 54c97296375ed0fb6ba00d9e78c5d269a77f0000 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Thu, 24 Mar 2022 21:27:16 +0100 Subject: [PATCH 25/30] Add path filter to CI workflow file --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a68bbc6..b3279fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,10 +4,16 @@ on: push: branches: - master + paths: + - FFMpegCore/** + - FFMpegCore.Test/** pull_request: branches: - master - release + paths: + - FFMpegCore/** + - FFMpegCore.Test/** jobs: ci: From 37ecbb8d875d65907b455c40aa5dca9601e4f317 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Thu, 24 Mar 2022 21:28:55 +0100 Subject: [PATCH 26/30] Add ci.yml to paths in CI workflow file --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3279fd..5afb8d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: branches: - master paths: + - .github/workflows/ci.yml - FFMpegCore/** - FFMpegCore.Test/** pull_request: @@ -12,6 +13,7 @@ on: - master - release paths: + - .github/workflows/ci.yml - FFMpegCore/** - FFMpegCore.Test/** From 42be852d30f0afb949ca314b4901bbf6945cedb9 Mon Sep 17 00:00:00 2001 From: Tieson Trowbridge Date: Sat, 26 Mar 2022 19:17:10 -0400 Subject: [PATCH 27/30] Update README.md Minor formatting fix for Path Configuration / Option 2 - indented code fence not parsed as a closing code block by Github's Markdown processor. --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index df5679a..7ed60ae 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,6 @@ A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and # API ## FFProbe - Use FFProbe to analyze media files: ```csharp @@ -22,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) @@ -193,7 +192,7 @@ await FFMpegArguments .Configure(options => options.WorkingDirectory = "./CurrentRunWorkingDir") .Configure(options => options.TemporaryFilesFolder = "./CurrentRunTmpFolder") .ProcessAsynchronously(); - ``` +``` ### Option 2 From 621b3a2eebfee07fe6f8e10382b568b4404d46fa Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Fri, 15 Apr 2022 12:08:03 +0200 Subject: [PATCH 28/30] Fix for PosterWithAudio #317 --- FFMpegCore/FFMpeg/FFMpeg.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index 4e7d47a..bceea02 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -254,9 +254,11 @@ public static bool PosterWithAudio(string image, string audio, string output) 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) From 9c0b2bdb442c25a92bccc83a329a7956dd5dae04 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Fri, 15 Apr 2022 12:55:34 +0200 Subject: [PATCH 29/30] Fix for JoinImageSequence #281 --- FFMpegCore.Test/VideoTest.cs | 2 +- FFMpegCore/FFMpeg/FFMpeg.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 56579e9..802f83e 100644 --- a/FFMpegCore.Test/VideoTest.cs +++ b/FFMpegCore.Test/VideoTest.cs @@ -468,7 +468,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); diff --git a/FFMpegCore/FFMpeg/FFMpeg.cs b/FFMpegCore/FFMpeg/FFMpeg.cs index bceea02..9e9e0ce 100644 --- a/FFMpegCore/FFMpeg/FFMpeg.cs +++ b/FFMpegCore/FFMpeg/FFMpeg.cs @@ -325,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(); From 4f38eed753767d7989212828f40f00479e793f17 Mon Sep 17 00:00:00 2001 From: Malte Rosenbjerg Date: Fri, 15 Apr 2022 15:08:30 +0200 Subject: [PATCH 30/30] Throw OperationCanceledException when processing is cancelled --- FFMpegCore.Test/VideoTest.cs | 94 +++++++++++++++---- .../FFMpeg/Arguments/InputPipeArgument.cs | 5 +- FFMpegCore/FFMpeg/Arguments/PipeArgument.cs | 5 +- FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs | 32 +++---- 4 files changed, 96 insertions(+), 40 deletions(-) diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs index 802f83e..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)] @@ -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/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/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/FFMpegArgumentProcessor.cs b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs index 34ccc59..43ace4d 100644 --- a/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs +++ b/FFMpegCore/FFMpeg/FFMpegArgumentProcessor.cs @@ -87,16 +87,17 @@ public bool ProcessSynchronously(bool throwOnError = true, FFOptions? ffMpegOpti { var options = GetConfiguredOptions(ffMpegOptions); var processArguments = PrepareProcessArguments(options, out var cancellationTokenSource); - processArguments.Exited += delegate { cancellationTokenSource.Cancel(); }; + IProcessResult? processResult = null; try { processResult = Process(processArguments, cancellationTokenSource).ConfigureAwait(false).GetAwaiter().GetResult(); } - catch (Exception e) + catch (OperationCanceledException) { - if (!HandleException(throwOnError, e, processResult?.ErrorData ?? Array.Empty())) return false; + if (throwOnError) + throw; } return HandleCompletion(throwOnError, processResult?.ExitCode ?? -1, processResult?.ErrorData ?? Array.Empty()); @@ -106,17 +107,18 @@ public async Task ProcessAsynchronously(bool throwOnError = true, FFOption { var options = GetConfiguredOptions(ffMpegOptions); var processArguments = PrepareProcessArguments(options, out var cancellationTokenSource); - + IProcessResult? processResult = null; try { processResult = await Process(processArguments, cancellationTokenSource).ConfigureAwait(false); } - catch (Exception e) + catch (OperationCanceledException) { - if (!HandleException(throwOnError, e, processResult?.ErrorData ?? Array.Empty())) return false; + if (throwOnError) + throw; } - + return HandleCompletion(throwOnError, processResult?.ExitCode ?? -1, processResult?.ErrorData ?? Array.Empty()); } @@ -127,8 +129,10 @@ private async Task Process(ProcessArguments processArguments, Ca _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)) @@ -148,6 +152,11 @@ await Task.WhenAll(instance.WaitForExitAsync().ContinueWith(t => _ffMpegArguments.Post(); }), _ffMpegArguments.During(cancellationTokenSource.Token)).ConfigureAwait(false); + if (cancelled) + { + throw new OperationCanceledException("ffmpeg processing was cancelled"); + } + return processResult; } finally @@ -209,15 +218,6 @@ private void ErrorData(object sender, string msg) _onError?.Invoke(msg); } - - private static bool HandleException(bool throwOnError, Exception e, IEnumerable errorData) - { - if (!throwOnError) - return false; - - throw new FFMpegException(FFMpegExceptionType.Process, "Exception thrown during processing", e, string.Join("\n", errorData)); - } - private void OutputData(object sender, string msg) { Debug.WriteLine(msg);