mirror of
https://github.com/rosenbjerg/FFMpegCore.git
synced 2024-11-10 08:34:12 +01:00
Merge pull request #273 from rosenbjerg/master
V.4.6.0
Former-commit-id: c618bcb1eb
This commit is contained in:
commit
d6f9866930
25 changed files with 711 additions and 70 deletions
|
@ -341,7 +341,22 @@ public void Builder_BuildString_SubtitleHardBurnFilter()
|
||||||
.WithParameter("PrimaryColour", "&HAA00FF00")))))
|
.WithParameter("PrimaryColour", "&HAA00FF00")))))
|
||||||
.Arguments;
|
.Arguments;
|
||||||
|
|
||||||
Assert.AreEqual("-i \"input.mp4\" -vf \"subtitles=sample.srt:charenc=UTF-8:original_size=1366x768:stream_index=0:force_style='FontName=DejaVu Serif\\,PrimaryColour=&HAA00FF00'\" \"output.mp4\"",
|
Assert.AreEqual("-i \"input.mp4\" -vf \"subtitles='sample.srt':charenc=UTF-8:original_size=1366x768:stream_index=0:force_style='FontName=DejaVu Serif\\,PrimaryColour=&HAA00FF00'\" \"output.mp4\"",
|
||||||
|
str);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Builder_BuildString_SubtitleHardBurnFilterFixedPaths()
|
||||||
|
{
|
||||||
|
var str = FFMpegArguments
|
||||||
|
.FromFileInput("input.mp4")
|
||||||
|
.OutputToFile("output.mp4", false, opt => opt
|
||||||
|
.WithVideoFilters(filterOptions => filterOptions
|
||||||
|
.HardBurnSubtitle(SubtitleHardBurnOptions
|
||||||
|
.Create(subtitlePath: @"sample( \ : [ ] , ' ).srt"))))
|
||||||
|
.Arguments;
|
||||||
|
|
||||||
|
Assert.AreEqual(@"-i ""input.mp4"" -vf ""subtitles='sample( \\ \: \[ \] \, '\\\'' ).srt'"" ""output.mp4""",
|
||||||
str);
|
str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -414,5 +429,60 @@ public void Builder_BuildString_ForcePixelFormat()
|
||||||
.OutputToFile("output.mp4", false, opt => opt.ForcePixelFormat("yuv444p")).Arguments;
|
.OutputToFile("output.mp4", false, opt => opt.ForcePixelFormat("yuv444p")).Arguments;
|
||||||
Assert.AreEqual("-i \"input.mp4\" -pix_fmt yuv444p \"output.mp4\"", str);
|
Assert.AreEqual("-i \"input.mp4\" -pix_fmt yuv444p \"output.mp4\"", str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Builder_BuildString_PanAudioFilterChannelNumber()
|
||||||
|
{
|
||||||
|
var str = FFMpegArguments.FromFileInput("input.mp4")
|
||||||
|
.OutputToFile("output.mp4", false,
|
||||||
|
opt => opt.WithAudioFilters(filterOptions => filterOptions.Pan(2, "c0=c1", "c1=c1")))
|
||||||
|
.Arguments;
|
||||||
|
|
||||||
|
Assert.AreEqual("-i \"input.mp4\" -af \"pan=2c|c0=c1|c1=c1\" \"output.mp4\"", str);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Builder_BuildString_PanAudioFilterChannelLayout()
|
||||||
|
{
|
||||||
|
var str = FFMpegArguments.FromFileInput("input.mp4")
|
||||||
|
.OutputToFile("output.mp4", false,
|
||||||
|
opt => opt.WithAudioFilters(filterOptions => filterOptions.Pan("stereo", "c0=c0", "c1=c1")))
|
||||||
|
.Arguments;
|
||||||
|
|
||||||
|
Assert.AreEqual("-i \"input.mp4\" -af \"pan=stereo|c0=c0|c1=c1\" \"output.mp4\"", str);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Builder_BuildString_PanAudioFilterChannelNoOutputDefinition()
|
||||||
|
{
|
||||||
|
var str = FFMpegArguments.FromFileInput("input.mp4")
|
||||||
|
.OutputToFile("output.mp4", false,
|
||||||
|
opt => opt.WithAudioFilters(filterOptions => filterOptions.Pan("stereo")))
|
||||||
|
.Arguments;
|
||||||
|
|
||||||
|
Assert.AreEqual("-i \"input.mp4\" -af \"pan=stereo\" \"output.mp4\"", str);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void Builder_BuildString_DynamicAudioNormalizerDefaultFormat()
|
||||||
|
{
|
||||||
|
var str = FFMpegArguments.FromFileInput("input.mp4")
|
||||||
|
.OutputToFile("output.mp4", false,
|
||||||
|
opt => opt.WithAudioFilters(filterOptions => filterOptions.DynamicNormalizer()))
|
||||||
|
.Arguments;
|
||||||
|
|
||||||
|
Assert.AreEqual("-i \"input.mp4\" -af \"dynaudnorm=f=500:g=31:p=0.95:m=10.0:r=0.0:n=1:c=0:b=0:s=0.0\" \"output.mp4\"", str);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
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)))
|
||||||
|
.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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
using FFMpegCore.Enums;
|
using FFMpegCore.Enums;
|
||||||
using FFMpegCore.Exceptions;
|
using FFMpegCore.Exceptions;
|
||||||
|
using FFMpegCore.Extend;
|
||||||
using FFMpegCore.Pipes;
|
using FFMpegCore.Pipes;
|
||||||
using FFMpegCore.Test.Resources;
|
using FFMpegCore.Test.Resources;
|
||||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
@ -8,7 +9,6 @@
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using FFMpegCore.Extend;
|
|
||||||
|
|
||||||
namespace FFMpegCore.Test
|
namespace FFMpegCore.Test
|
||||||
{
|
{
|
||||||
|
@ -223,5 +223,111 @@ public void Audio_ToAAC_Args_Pipe_InvalidSampleRate()
|
||||||
.WithAudioCodec(AudioCodec.Aac))
|
.WithAudioCodec(AudioCodec.Aac))
|
||||||
.ProcessSynchronously());
|
.ProcessSynchronously());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod, Timeout(10000)]
|
||||||
|
public void Audio_Pan_ToMono()
|
||||||
|
{
|
||||||
|
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
|
||||||
|
|
||||||
|
var success = FFMpegArguments.FromFileInput(TestResources.Mp3Audio)
|
||||||
|
.OutputToFile(outputFile, true,
|
||||||
|
argumentOptions => argumentOptions
|
||||||
|
.WithAudioFilters(filter => filter.Pan(1, "c0 < 0.9 * c0 + 0.1 * c1")))
|
||||||
|
.ProcessSynchronously();
|
||||||
|
|
||||||
|
var mediaAnalysis = FFProbe.Analyse(outputFile);
|
||||||
|
|
||||||
|
Assert.IsTrue(success);
|
||||||
|
Assert.AreEqual(1, mediaAnalysis.AudioStreams.Count);
|
||||||
|
Assert.AreEqual("mono", mediaAnalysis.PrimaryAudioStream.ChannelLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod, Timeout(10000)]
|
||||||
|
public void Audio_Pan_ToMonoNoDefinitions()
|
||||||
|
{
|
||||||
|
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
|
||||||
|
|
||||||
|
var success = FFMpegArguments.FromFileInput(TestResources.Mp3Audio)
|
||||||
|
.OutputToFile(outputFile, true,
|
||||||
|
argumentOptions => argumentOptions
|
||||||
|
.WithAudioFilters(filter => filter.Pan(1)))
|
||||||
|
.ProcessSynchronously();
|
||||||
|
|
||||||
|
var mediaAnalysis = FFProbe.Analyse(outputFile);
|
||||||
|
|
||||||
|
Assert.IsTrue(success);
|
||||||
|
Assert.AreEqual(1, mediaAnalysis.AudioStreams.Count);
|
||||||
|
Assert.AreEqual("mono", mediaAnalysis.PrimaryAudioStream.ChannelLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod, Timeout(10000)]
|
||||||
|
public void Audio_Pan_ToMonoChannelsToOutputDefinitionsMismatch()
|
||||||
|
{
|
||||||
|
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
|
||||||
|
|
||||||
|
var ex = Assert.ThrowsException<ArgumentException>(() => FFMpegArguments.FromFileInput(TestResources.Mp3Audio)
|
||||||
|
.OutputToFile(outputFile, true,
|
||||||
|
argumentOptions => argumentOptions
|
||||||
|
.WithAudioFilters(filter => filter.Pan(1, "c0=c0", "c1=c1")))
|
||||||
|
.ProcessSynchronously());
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod, Timeout(10000)]
|
||||||
|
public void Audio_Pan_ToMonoChannelsLayoutToOutputDefinitionsMismatch()
|
||||||
|
{
|
||||||
|
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
|
||||||
|
|
||||||
|
var ex = Assert.ThrowsException<FFMpegException>(() => FFMpegArguments.FromFileInput(TestResources.Mp3Audio)
|
||||||
|
.OutputToFile(outputFile, true,
|
||||||
|
argumentOptions => argumentOptions
|
||||||
|
.WithAudioFilters(filter => filter.Pan("mono", "c0=c0", "c1=c1")))
|
||||||
|
.ProcessSynchronously());
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod, Timeout(10000)]
|
||||||
|
public void Audio_DynamicNormalizer_WithDefaultValues()
|
||||||
|
{
|
||||||
|
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
|
||||||
|
|
||||||
|
var success = FFMpegArguments.FromFileInput(TestResources.Mp3Audio)
|
||||||
|
.OutputToFile(outputFile, true,
|
||||||
|
argumentOptions => argumentOptions
|
||||||
|
.WithAudioFilters(filter => filter.DynamicNormalizer()))
|
||||||
|
.ProcessSynchronously();
|
||||||
|
|
||||||
|
Assert.IsTrue(success);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod, Timeout(10000)]
|
||||||
|
public void Audio_DynamicNormalizer_WithNonDefaultValues()
|
||||||
|
{
|
||||||
|
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
|
||||||
|
|
||||||
|
var success = FFMpegArguments.FromFileInput(TestResources.Mp3Audio)
|
||||||
|
.OutputToFile(outputFile, true,
|
||||||
|
argumentOptions => argumentOptions
|
||||||
|
.WithAudioFilters(
|
||||||
|
filter => filter.DynamicNormalizer(250, 7, 0.9, 2, 1, false, true, true, 0.5)))
|
||||||
|
.ProcessSynchronously();
|
||||||
|
|
||||||
|
Assert.IsTrue(success);
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataTestMethod, Timeout(10000)]
|
||||||
|
[DataRow(2)]
|
||||||
|
[DataRow(32)]
|
||||||
|
[DataRow(8)]
|
||||||
|
public void Audio_DynamicNormalizer_FilterWindow(int filterWindow)
|
||||||
|
{
|
||||||
|
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
|
||||||
|
|
||||||
|
var ex = Assert.ThrowsException<ArgumentOutOfRangeException>(() => FFMpegArguments
|
||||||
|
.FromFileInput(TestResources.Mp3Audio)
|
||||||
|
.OutputToFile(outputFile, true,
|
||||||
|
argumentOptions => argumentOptions
|
||||||
|
.WithAudioFilters(
|
||||||
|
filter => filter.DynamicNormalizer(filterWindow: filterWindow)))
|
||||||
|
.ProcessSynchronously());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -40,9 +40,9 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="GitHubActionsTestLogger" Version="1.2.0" />
|
<PackageReference Include="GitHubActionsTestLogger" Version="1.2.0" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
|
||||||
<PackageReference Include="MSTest.TestAdapter" Version="2.2.1" />
|
<PackageReference Include="MSTest.TestAdapter" Version="2.2.7" />
|
||||||
<PackageReference Include="MSTest.TestFramework" Version="2.2.1" />
|
<PackageReference Include="MSTest.TestFramework" Version="2.2.7" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using FFMpegCore.Test.Resources;
|
using FFMpegCore.Test.Resources;
|
||||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
@ -25,6 +26,30 @@ public async Task Audio_FromStream_Duration()
|
||||||
Assert.IsTrue(fileAnalysis.Duration == streamAnalysis.Duration);
|
Assert.IsTrue(fileAnalysis.Duration == streamAnalysis.Duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void FrameAnalysis_Sync()
|
||||||
|
{
|
||||||
|
var frameAnalysis = FFProbe.GetFrames(TestResources.WebmVideo);
|
||||||
|
|
||||||
|
Assert.AreEqual(90, frameAnalysis.Frames.Count);
|
||||||
|
Assert.IsTrue(frameAnalysis.Frames.All(f => f.PixelFormat == "yuv420p"));
|
||||||
|
Assert.IsTrue(frameAnalysis.Frames.All(f => f.Height == 360));
|
||||||
|
Assert.IsTrue(frameAnalysis.Frames.All(f => f.Width == 640));
|
||||||
|
Assert.IsTrue(frameAnalysis.Frames.All(f => f.MediaType == "video"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task FrameAnalysis_Async()
|
||||||
|
{
|
||||||
|
var frameAnalysis = await FFProbe.GetFramesAsync(TestResources.WebmVideo);
|
||||||
|
|
||||||
|
Assert.AreEqual(90, frameAnalysis.Frames.Count);
|
||||||
|
Assert.IsTrue(frameAnalysis.Frames.All(f => f.PixelFormat == "yuv420p"));
|
||||||
|
Assert.IsTrue(frameAnalysis.Frames.All(f => f.Height == 360));
|
||||||
|
Assert.IsTrue(frameAnalysis.Frames.All(f => f.Width == 640));
|
||||||
|
Assert.IsTrue(frameAnalysis.Frames.All(f => f.MediaType == "video"));
|
||||||
|
}
|
||||||
|
|
||||||
[DataTestMethod]
|
[DataTestMethod]
|
||||||
[DataRow("0:00:03.008000", 0, 0, 0, 3, 8)]
|
[DataRow("0:00:03.008000", 0, 0, 0, 3, 8)]
|
||||||
[DataRow("05:12:59.177", 0, 5, 12, 59, 177)]
|
[DataRow("05:12:59.177", 0, 5, 12, 59, 177)]
|
||||||
|
@ -114,5 +139,15 @@ public async Task Probe_Success_Subtitle_Async()
|
||||||
Assert.AreEqual(0, info.AudioStreams.Count);
|
Assert.AreEqual(0, info.AudioStreams.Count);
|
||||||
Assert.AreEqual(0, info.VideoStreams.Count);
|
Assert.AreEqual(0, info.VideoStreams.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod, Timeout(10000)]
|
||||||
|
public async Task Probe_Success_Disposition_Async()
|
||||||
|
{
|
||||||
|
var info = await FFProbe.AnalyseAsync(TestResources.Mp4Video);
|
||||||
|
Assert.IsNotNull(info.PrimaryAudioStream);
|
||||||
|
Assert.IsNotNull(info.PrimaryAudioStream.Disposition);
|
||||||
|
Assert.AreEqual(true, info.PrimaryAudioStream.Disposition["default"]);
|
||||||
|
Assert.AreEqual(false, info.PrimaryAudioStream.Disposition["forced"]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -49,7 +49,7 @@ public async Task SerializeAsync(Stream stream, CancellationToken token)
|
||||||
{
|
{
|
||||||
var buffer = new byte[data.Stride * data.Height];
|
var buffer = new byte[data.Stride * data.Height];
|
||||||
Marshal.Copy(data.Scan0, buffer, 0, buffer.Length);
|
Marshal.Copy(data.Scan0, buffer, 0, buffer.Length);
|
||||||
await stream.WriteAsync(buffer, 0, buffer.Length, token);
|
await stream.WriteAsync(buffer, 0, buffer.Length, token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|
|
@ -16,7 +16,7 @@ internal static class KeyValuePairExtensions
|
||||||
public static string FormatArgumentPair(this KeyValuePair<string, string> pair, bool enclose)
|
public static string FormatArgumentPair(this KeyValuePair<string, string> pair, bool enclose)
|
||||||
{
|
{
|
||||||
var key = pair.Key;
|
var key = pair.Key;
|
||||||
var value = enclose ? pair.Value.EncloseIfContainsSpace() : pair.Value;
|
var value = enclose ? StringExtensions.EncloseIfContainsSpace(pair.Value) : pair.Value;
|
||||||
|
|
||||||
return $"{key}={value}";
|
return $"{key}={value}";
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ public void Serialize(Stream stream)
|
||||||
|
|
||||||
public async Task SerializeAsync(Stream stream, CancellationToken token)
|
public async Task SerializeAsync(Stream stream, CancellationToken token)
|
||||||
{
|
{
|
||||||
await stream.WriteAsync(_sample, 0, _sample.Length, token);
|
await stream.WriteAsync(_sample, 0, _sample.Length, token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,70 @@
|
||||||
namespace FFMpegCore.Extend
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace FFMpegCore.Extend
|
||||||
{
|
{
|
||||||
internal static class StringExtensions
|
internal static class StringExtensions
|
||||||
{
|
{
|
||||||
|
private static Dictionary<char, string> CharactersSubstitution { get; } = new Dictionary<char, string>
|
||||||
|
{
|
||||||
|
{ '\\', @"\\" },
|
||||||
|
{ ':', @"\:" },
|
||||||
|
{ '[', @"\[" },
|
||||||
|
{ ']', @"\]" },
|
||||||
|
{ '\'', @"'\\\''" }
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enclose string between quotes if contains an space character
|
/// Enclose string between quotes if contains an space character
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="input">The input</param>
|
/// <param name="input">The input</param>
|
||||||
/// <returns>The enclosed string</returns>
|
/// <returns>The enclosed string</returns>
|
||||||
public static string EncloseIfContainsSpace(this string input)
|
public static string EncloseIfContainsSpace(string input)
|
||||||
{
|
{
|
||||||
return input.Contains(" ") ? $"'{input}'" : input;
|
return input.Contains(" ") ? $"'{input}'" : input;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enclose an string in quotes
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string EncloseInQuotes(string input)
|
||||||
|
{
|
||||||
|
return $"'{input}'";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scape several characters in subtitle path used by FFmpeg
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This is needed because internally FFmpeg use Libav Filters
|
||||||
|
/// and the info send to it must be in an specific format
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="source"></param>
|
||||||
|
/// <returns>Scaped path</returns>
|
||||||
|
public static string ToFFmpegLibavfilterPath(string source)
|
||||||
|
{
|
||||||
|
return source.Replace(CharactersSubstitution);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Replace(this string str, Dictionary<char, string> replaceList)
|
||||||
|
{
|
||||||
|
var parsedString = new StringBuilder();
|
||||||
|
|
||||||
|
foreach (var l in str)
|
||||||
|
{
|
||||||
|
if (replaceList.ContainsKey(l))
|
||||||
|
{
|
||||||
|
parsedString.Append(replaceList[l]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
parsedString.Append(l);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedString.ToString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
60
FFMpegCore/FFMpeg/Arguments/AudioFiltersArgument.cs
Normal file
60
FFMpegCore/FFMpeg/Arguments/AudioFiltersArgument.cs
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using FFMpegCore.Exceptions;
|
||||||
|
|
||||||
|
namespace FFMpegCore.Arguments
|
||||||
|
{
|
||||||
|
public class AudioFiltersArgument : IArgument
|
||||||
|
{
|
||||||
|
public readonly AudioFilterOptions Options;
|
||||||
|
|
||||||
|
public AudioFiltersArgument(AudioFilterOptions options)
|
||||||
|
{
|
||||||
|
Options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Text => GetText();
|
||||||
|
|
||||||
|
private string GetText()
|
||||||
|
{
|
||||||
|
if (!Options.Arguments.Any())
|
||||||
|
throw new FFMpegArgumentException("No audio-filter arguments provided");
|
||||||
|
|
||||||
|
var arguments = Options.Arguments
|
||||||
|
.Where(arg => !string.IsNullOrEmpty(arg.Value))
|
||||||
|
.Select(arg =>
|
||||||
|
{
|
||||||
|
var escapedValue = arg.Value.Replace(",", "\\,");
|
||||||
|
return string.IsNullOrEmpty(arg.Key) ? escapedValue : $"{arg.Key}={escapedValue}";
|
||||||
|
});
|
||||||
|
|
||||||
|
return $"-af \"{string.Join(", ", arguments)}\"";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IAudioFilterArgument
|
||||||
|
{
|
||||||
|
public string Key { get; }
|
||||||
|
public string Value { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AudioFilterOptions
|
||||||
|
{
|
||||||
|
public List<IAudioFilterArgument> Arguments { get; } = new List<IAudioFilterArgument>();
|
||||||
|
|
||||||
|
public AudioFilterOptions Pan(string channelLayout, params string[] outputDefinitions) => WithArgument(new PanArgument(channelLayout, outputDefinitions));
|
||||||
|
public AudioFilterOptions Pan(int channels, params string[] outputDefinitions) => WithArgument(new PanArgument(channels, outputDefinitions));
|
||||||
|
public AudioFilterOptions DynamicNormalizer(int frameLength = 500, int filterWindow = 31, double targetPeak = 0.95,
|
||||||
|
double gainFactor = 10.0, double targetRms = 0.0, bool channelCoupling = true,
|
||||||
|
bool enableDcBiasCorrection = false, bool enableAlternativeBoundary = false,
|
||||||
|
double compressorFactor = 0.0) => WithArgument(new DynamicNormalizerArgument(frameLength, filterWindow,
|
||||||
|
targetPeak, gainFactor, targetRms, channelCoupling, enableDcBiasCorrection, enableAlternativeBoundary,
|
||||||
|
compressorFactor));
|
||||||
|
|
||||||
|
private AudioFilterOptions WithArgument(IAudioFilterArgument argument)
|
||||||
|
{
|
||||||
|
Arguments.Add(argument);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
49
FFMpegCore/FFMpeg/Arguments/DynamicNormalizerArgument.cs
Normal file
49
FFMpegCore/FFMpeg/Arguments/DynamicNormalizerArgument.cs
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace FFMpegCore.Arguments
|
||||||
|
{
|
||||||
|
public class DynamicNormalizerArgument : IAudioFilterArgument
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, string> _arguments = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dynamic Audio Normalizer. <see href="https://ffmpeg.org/ffmpeg-filters.html#dynaudnorm"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="frameLength">Set the frame length in milliseconds. Must be between 10 to 8000. The default value is 500</param>
|
||||||
|
/// <param name="filterWindow">Set the Gaussian filter window size. In range from 3 to 301, must be odd number. The default value is 31</param>
|
||||||
|
/// <param name="targetPeak">Set the target peak value. The default value is 0.95</param>
|
||||||
|
/// <param name="gainFactor">Set the maximum gain factor. In range from 1.0 to 100.0. Default is 10.0.</param>
|
||||||
|
/// <param name="targetRms">Set the target RMS. In range from 0.0 to 1.0. Default to 0.0 (disabled)</param>
|
||||||
|
/// <param name="channelCoupling">Enable channels coupling. By default is enabled.</param>
|
||||||
|
/// <param name="enableDcBiasCorrection">Enable DC bias correction. By default is disabled.</param>
|
||||||
|
/// <param name="enableAlternativeBoundary">Enable alternative boundary mode. By default is disabled.</param>
|
||||||
|
/// <param name="compressorFactor">Set the compress factor. In range from 0.0 to 30.0. Default is 0.0 (disabled).</param>
|
||||||
|
public DynamicNormalizerArgument(int frameLength = 500, int filterWindow = 31, double targetPeak = 0.95, double gainFactor = 10.0, double targetRms = 0.0, bool channelCoupling = true, bool enableDcBiasCorrection = false, bool enableAlternativeBoundary = false, double compressorFactor = 0.0)
|
||||||
|
{
|
||||||
|
if (frameLength < 10 || frameLength > 8000) throw new ArgumentOutOfRangeException(nameof(frameLength),"Frame length must be between 10 to 8000");
|
||||||
|
if (filterWindow < 3 || filterWindow > 31) throw new ArgumentOutOfRangeException(nameof(filterWindow), "Gaussian filter window size must be between 3 to 31");
|
||||||
|
if (filterWindow % 2 == 0) throw new ArgumentOutOfRangeException(nameof(filterWindow), "Gaussian filter window size must be an odd number");
|
||||||
|
if (targetPeak <= 0 || targetPeak > 1) throw new ArgumentOutOfRangeException(nameof(targetPeak));
|
||||||
|
if (gainFactor < 1 || gainFactor > 100) throw new ArgumentOutOfRangeException(nameof(gainFactor), "Gain factor must be between 1.0 to 100.0");
|
||||||
|
if (targetRms < 0 || targetRms > 1) throw new ArgumentOutOfRangeException(nameof(targetRms), "Target RMS must be between 0.0 and 1.0");
|
||||||
|
if (compressorFactor < 0 || compressorFactor > 30) throw new ArgumentOutOfRangeException(nameof(compressorFactor), "Compressor factor must be between 0.0 and 30.0");
|
||||||
|
|
||||||
|
_arguments.Add("f", frameLength.ToString());
|
||||||
|
_arguments.Add("g", filterWindow.ToString());
|
||||||
|
_arguments.Add("p", targetPeak.ToString("0.00", CultureInfo.InvariantCulture));
|
||||||
|
_arguments.Add("m", gainFactor.ToString("0.0", CultureInfo.InvariantCulture));
|
||||||
|
_arguments.Add("r", targetRms.ToString("0.0", CultureInfo.InvariantCulture));
|
||||||
|
_arguments.Add("n", (channelCoupling ? 1 : 0).ToString());
|
||||||
|
_arguments.Add("c", (enableDcBiasCorrection ? 1 : 0).ToString());
|
||||||
|
_arguments.Add("b", (enableAlternativeBoundary ? 1 : 0).ToString());
|
||||||
|
_arguments.Add("s", compressorFactor.ToString("0.0", CultureInfo.InvariantCulture));
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Key { get; } = "dynaudnorm";
|
||||||
|
|
||||||
|
public string Value => string.Join(":", _arguments.Select(pair => $"{pair.Key}={pair.Value}"));
|
||||||
|
}
|
||||||
|
}
|
59
FFMpegCore/FFMpeg/Arguments/PanArgument.cs
Normal file
59
FFMpegCore/FFMpeg/Arguments/PanArgument.cs
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace FFMpegCore.Arguments
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Mix channels with specific gain levels.
|
||||||
|
/// </summary>
|
||||||
|
public class PanArgument : IAudioFilterArgument
|
||||||
|
{
|
||||||
|
public readonly string ChannelLayout;
|
||||||
|
private readonly string[] _outputDefinitions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mix channels with specific gain levels <see href="https://ffmpeg.org/ffmpeg-filters.html#toc-pan-1"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="channelLayout">
|
||||||
|
/// Represent the output channel layout. Like "stereo", "mono", "2.1", "5.1"
|
||||||
|
/// </param>
|
||||||
|
/// <param name="outputDefinitions">
|
||||||
|
/// Output channel specification, of the form: "out_name=[gain*]in_name[(+-)[gain*]in_name...]"
|
||||||
|
/// </param>
|
||||||
|
public PanArgument(string channelLayout, params string[] outputDefinitions)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(channelLayout))
|
||||||
|
{
|
||||||
|
throw new ArgumentException("The channel layout must be set" ,nameof(channelLayout));
|
||||||
|
}
|
||||||
|
|
||||||
|
ChannelLayout = channelLayout;
|
||||||
|
|
||||||
|
_outputDefinitions = outputDefinitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mix channels with specific gain levels <see href="https://ffmpeg.org/ffmpeg-filters.html#toc-pan-1"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="channels">Number of channels in output file</param>
|
||||||
|
/// <param name="outputDefinitions">
|
||||||
|
/// Output channel specification, of the form: "out_name=[gain*]in_name[(+-)[gain*]in_name...]"
|
||||||
|
/// </param>
|
||||||
|
public PanArgument(int channels, params string[] outputDefinitions)
|
||||||
|
{
|
||||||
|
if (channels <= 0) throw new ArgumentOutOfRangeException(nameof(channels));
|
||||||
|
|
||||||
|
if (outputDefinitions.Length > channels)
|
||||||
|
throw new ArgumentException("The number of output definitions must be equal or lower than number of channels", nameof(outputDefinitions));
|
||||||
|
|
||||||
|
ChannelLayout = $"{channels}c";
|
||||||
|
|
||||||
|
_outputDefinitions = outputDefinitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Key { get; } = "pan";
|
||||||
|
|
||||||
|
public string Value =>
|
||||||
|
string.Join("|", Enumerable.Empty<string>().Append(ChannelLayout).Concat(_outputDefinitions));
|
||||||
|
}
|
||||||
|
}
|
|
@ -40,7 +40,7 @@ public async Task During(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await ProcessDataAsync(cancellationToken);
|
await ProcessDataAsync(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (TaskCanceledException)
|
catch (TaskCanceledException)
|
||||||
{
|
{
|
||||||
|
|
|
@ -103,7 +103,9 @@ public SubtitleHardBurnOptions WithParameter(string key, string value)
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal string TextInternal => string.Join(":", new[] { _subtitle.EncloseIfContainsSpace() }.Concat(Parameters.Select(parameter => parameter.FormatArgumentPair(enclose: true))));
|
internal string TextInternal => string
|
||||||
|
.Join(":", new[] { StringExtensions.EncloseInQuotes(StringExtensions.ToFFmpegLibavfilterPath(_subtitle)) }
|
||||||
|
.Concat(Parameters.Select(parameter => parameter.FormatArgumentPair(enclose: true))));
|
||||||
}
|
}
|
||||||
|
|
||||||
public class StyleOptions
|
public class StyleOptions
|
||||||
|
|
|
@ -50,7 +50,7 @@ public static async Task<bool> SnapshotAsync(string input, string output, Size?
|
||||||
if (Path.GetExtension(output) != FileExtension.Png)
|
if (Path.GetExtension(output) != FileExtension.Png)
|
||||||
output = Path.GetFileNameWithoutExtension(output) + FileExtension.Png;
|
output = Path.GetFileNameWithoutExtension(output) + FileExtension.Png;
|
||||||
|
|
||||||
var source = await FFProbe.AnalyseAsync(input);
|
var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false);
|
||||||
var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex);
|
var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex);
|
||||||
|
|
||||||
return await arguments
|
return await arguments
|
||||||
|
@ -93,7 +93,7 @@ public static Bitmap Snapshot(string input, Size? size = null, TimeSpan? capture
|
||||||
/// <returns>Bitmap with the requested snapshot.</returns>
|
/// <returns>Bitmap with the requested snapshot.</returns>
|
||||||
public static async Task<Bitmap> SnapshotAsync(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0)
|
public static async Task<Bitmap> SnapshotAsync(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0)
|
||||||
{
|
{
|
||||||
var source = await FFProbe.AnalyseAsync(input);
|
var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false);
|
||||||
var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex);
|
var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex);
|
||||||
using var ms = new MemoryStream();
|
using var ms = new MemoryStream();
|
||||||
|
|
||||||
|
@ -116,7 +116,9 @@ private static (FFMpegArguments, Action<FFMpegArgumentOptions> outputOptions) Bu
|
||||||
{
|
{
|
||||||
captureTime ??= TimeSpan.FromSeconds(source.Duration.TotalSeconds / 3);
|
captureTime ??= TimeSpan.FromSeconds(source.Duration.TotalSeconds / 3);
|
||||||
size = PrepareSnapshotSize(source, size);
|
size = PrepareSnapshotSize(source, size);
|
||||||
streamIndex = streamIndex == null ? 0 : source.VideoStreams.FirstOrDefault(videoStream => videoStream.Index == streamIndex).Index;
|
streamIndex ??= source.PrimaryVideoStream?.Index
|
||||||
|
?? source.VideoStreams.FirstOrDefault()?.Index
|
||||||
|
?? 0;
|
||||||
|
|
||||||
return (FFMpegArguments
|
return (FFMpegArguments
|
||||||
.FromFileInput(input, false, options => options
|
.FromFileInput(input, false, options => options
|
||||||
|
@ -301,12 +303,13 @@ public static bool Join(string output, params string[] videos)
|
||||||
public static bool JoinImageSequence(string output, double frameRate = 30, params ImageInfo[] images)
|
public static bool JoinImageSequence(string output, double frameRate = 30, params ImageInfo[] images)
|
||||||
{
|
{
|
||||||
var tempFolderName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, Guid.NewGuid().ToString());
|
var tempFolderName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, Guid.NewGuid().ToString());
|
||||||
var temporaryImageFiles = images.Select((image, index) =>
|
var temporaryImageFiles = images.Select((imageInfo, index) =>
|
||||||
{
|
{
|
||||||
FFMpegHelper.ConversionSizeExceptionCheck(Image.FromFile(image.FullName));
|
using var image = Image.FromFile(imageInfo.FullName);
|
||||||
var destinationPath = Path.Combine(tempFolderName, $"{index.ToString().PadLeft(9, '0')}{image.Extension}");
|
FFMpegHelper.ConversionSizeExceptionCheck(image);
|
||||||
|
var destinationPath = Path.Combine(tempFolderName, $"{index.ToString().PadLeft(9, '0')}{imageInfo.Extension}");
|
||||||
Directory.CreateDirectory(tempFolderName);
|
Directory.CreateDirectory(tempFolderName);
|
||||||
File.Copy(image.FullName, destinationPath);
|
File.Copy(imageInfo.FullName, destinationPath);
|
||||||
return destinationPath;
|
return destinationPath;
|
||||||
}).ToArray();
|
}).ToArray();
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,13 @@ public FFMpegArgumentOptions WithVideoFilters(Action<VideoFilterOptions> videoFi
|
||||||
return WithArgument(new VideoFiltersArgument(videoFilterOptionsObj));
|
return WithArgument(new VideoFiltersArgument(videoFilterOptionsObj));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public FFMpegArgumentOptions WithAudioFilters(Action<AudioFilterOptions> audioFilterOptions)
|
||||||
|
{
|
||||||
|
var audioFilterOptionsObj = new AudioFilterOptions();
|
||||||
|
audioFilterOptions(audioFilterOptionsObj);
|
||||||
|
return WithArgument(new AudioFiltersArgument(audioFilterOptionsObj));
|
||||||
|
}
|
||||||
|
|
||||||
public FFMpegArgumentOptions WithFramerate(double framerate) => WithArgument(new FrameRateArgument(framerate));
|
public FFMpegArgumentOptions WithFramerate(double framerate) => WithArgument(new FrameRateArgument(framerate));
|
||||||
public FFMpegArgumentOptions WithoutMetadata() => WithArgument(new RemoveMetadataArgument());
|
public FFMpegArgumentOptions WithoutMetadata() => WithArgument(new RemoveMetadataArgument());
|
||||||
public FFMpegArgumentOptions WithSpeedPreset(Speed speed) => WithArgument(new SpeedPresetArgument(speed));
|
public FFMpegArgumentOptions WithSpeedPreset(Speed speed) => WithArgument(new SpeedPresetArgument(speed));
|
||||||
|
|
|
@ -75,13 +75,7 @@ void OnCancelEvent(object sender, int timeout)
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_ffMpegArguments.Pre();
|
errorCode = Process(instance, cancellationTokenSource).ConfigureAwait(false).GetAwaiter().GetResult();
|
||||||
Task.WaitAll(instance.FinishedRunning().ContinueWith(t =>
|
|
||||||
{
|
|
||||||
errorCode = t.Result;
|
|
||||||
cancellationTokenSource.Cancel();
|
|
||||||
_ffMpegArguments.Post();
|
|
||||||
}), _ffMpegArguments.During(cancellationTokenSource.Token));
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
@ -114,13 +108,7 @@ void OnCancelEvent(object sender, int timeout)
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_ffMpegArguments.Pre();
|
errorCode = await Process(instance, cancellationTokenSource).ConfigureAwait(false);
|
||||||
await Task.WhenAll(instance.FinishedRunning().ContinueWith(t =>
|
|
||||||
{
|
|
||||||
errorCode = t.Result;
|
|
||||||
cancellationTokenSource.Cancel();
|
|
||||||
_ffMpegArguments.Post();
|
|
||||||
}), _ffMpegArguments.During(cancellationTokenSource.Token)).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
@ -134,6 +122,21 @@ await Task.WhenAll(instance.FinishedRunning().ContinueWith(t =>
|
||||||
return HandleCompletion(throwOnError, errorCode, instance.ErrorData);
|
return HandleCompletion(throwOnError, errorCode, instance.ErrorData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<int> 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<string> errorData)
|
private bool HandleCompletion(bool throwOnError, int exitCode, IReadOnlyList<string> errorData)
|
||||||
{
|
{
|
||||||
if (throwOnError && exitCode != 0)
|
if (throwOnError && exitCode != 0)
|
||||||
|
@ -145,17 +148,18 @@ private bool HandleCompletion(bool throwOnError, int exitCode, IReadOnlyList<str
|
||||||
return exitCode == 0;
|
return exitCode == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Instance PrepareInstance(FFOptions ffMpegOptions,
|
private Instance PrepareInstance(FFOptions ffOptions,
|
||||||
out CancellationTokenSource cancellationTokenSource)
|
out CancellationTokenSource cancellationTokenSource)
|
||||||
{
|
{
|
||||||
FFMpegHelper.RootExceptionCheck();
|
FFMpegHelper.RootExceptionCheck();
|
||||||
FFMpegHelper.VerifyFFMpegExists(ffMpegOptions);
|
FFMpegHelper.VerifyFFMpegExists(ffOptions);
|
||||||
var startInfo = new ProcessStartInfo
|
var startInfo = new ProcessStartInfo
|
||||||
{
|
{
|
||||||
FileName = GlobalFFOptions.GetFFMpegBinaryPath(ffMpegOptions),
|
FileName = GlobalFFOptions.GetFFMpegBinaryPath(ffOptions),
|
||||||
Arguments = _ffMpegArguments.Text,
|
Arguments = _ffMpegArguments.Text,
|
||||||
StandardOutputEncoding = ffMpegOptions.Encoding,
|
StandardOutputEncoding = ffOptions.Encoding,
|
||||||
StandardErrorEncoding = ffMpegOptions.Encoding,
|
StandardErrorEncoding = ffOptions.Encoding,
|
||||||
|
WorkingDirectory = ffOptions.WorkingDirectory
|
||||||
};
|
};
|
||||||
var instance = new Instance(startInfo);
|
var instance = new Instance(startInfo);
|
||||||
cancellationTokenSource = new CancellationTokenSource();
|
cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
|
|
@ -20,8 +20,8 @@ public StreamPipeSink(Stream destination)
|
||||||
Writer = (inputStream, cancellationToken) => inputStream.CopyToAsync(destination, BlockSize, cancellationToken);
|
Writer = (inputStream, cancellationToken) => inputStream.CopyToAsync(destination, BlockSize, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task ReadAsync(Stream inputStream, CancellationToken cancellationToken)
|
public async Task ReadAsync(Stream inputStream, CancellationToken cancellationToken)
|
||||||
=> Writer(inputStream, cancellationToken);
|
=> await Writer(inputStream, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
public string GetFormat() => Format;
|
public string GetFormat() => Format;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,13 +8,18 @@
|
||||||
<Description>A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications</Description>
|
<Description>A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications</Description>
|
||||||
<AssemblyVersion>4.0.0.0</AssemblyVersion>
|
<AssemblyVersion>4.0.0.0</AssemblyVersion>
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
<PackageReleaseNotes>- Support for PCM audio samples (thanks to Namaneo)
|
<PackageReleaseNotes>- Support for reading Disposition metadata on streams in FFProbe output (thanks alex6dj)
|
||||||
- Support for subtitle streams in MediaAnalysis (thanks to alex6dj)
|
- Fix for stream index in Snapshot(Async) (thanks stasokrosh)
|
||||||
- Support for subtitle hard burning (thanks to alex6dj)
|
- Add support for frame analysis through FFProbe,GetFrames(Async) (thanks andyjmorgan)
|
||||||
- Additional codec* properties on MediaAnalysis object (thanks to GuyWithDogs)
|
- Fix for subtitle burning error, when subtitle path contains special characters (thanks alex6dj)
|
||||||
- SelectStream method for mapping/specifyíng specific streams from input files (thanks to Feodoros)</PackageReleaseNotes>
|
- Support for Audio filters (thanks alex6dj)
|
||||||
|
- Fix FFProbe.AnalyseAsync not throwing if non-zero exit-code (thanks samburovkv)
|
||||||
|
- Change bit_rate from int to long to support analysis of high bit-rate files (thanks zhuker)
|
||||||
|
- Support for specifying working directory for ffmpeg and ffprobe processes through FFOptions
|
||||||
|
- Ensure Image instances in JoinImageSequence are disposed
|
||||||
|
- Added ConfigureAwait(false) to prevent hanging with certain frameworks</PackageReleaseNotes>
|
||||||
<LangVersion>8</LangVersion>
|
<LangVersion>8</LangVersion>
|
||||||
<PackageVersion>4.5.0</PackageVersion>
|
<PackageVersion>4.6.0</PackageVersion>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
<Authors>Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev</Authors>
|
<Authors>Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev</Authors>
|
||||||
<PackageTags>ffmpeg ffprobe convert video audio mediafile resize analyze muxing</PackageTags>
|
<PackageTags>ffmpeg ffprobe convert video audio mediafile resize analyze muxing</PackageTags>
|
||||||
|
|
|
@ -6,6 +6,11 @@ namespace FFMpegCore
|
||||||
{
|
{
|
||||||
public class FFOptions
|
public class FFOptions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Working directory for the ffmpeg/ffprobe instance
|
||||||
|
/// </summary>
|
||||||
|
public string WorkingDirectory { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Folder container ffmpeg and ffprobe binaries. Leave empty if ffmpeg and ffprobe are present in PATH
|
/// Folder container ffmpeg and ffprobe binaries. Leave empty if ffmpeg and ffprobe are present in PATH
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -18,16 +18,28 @@ public static IMediaAnalysis Analyse(string filePath, int outputCapacity = int.M
|
||||||
if (!File.Exists(filePath))
|
if (!File.Exists(filePath))
|
||||||
throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'");
|
throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'");
|
||||||
|
|
||||||
using var instance = PrepareInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
|
using var instance = PrepareStreamAnalysisInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
|
||||||
var exitCode = instance.BlockUntilFinished();
|
var exitCode = instance.BlockUntilFinished();
|
||||||
if (exitCode != 0)
|
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));
|
throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({exitCode} - {string.Join("\n", instance.ErrorData)})", null, string.Join("\n", instance.ErrorData));
|
||||||
|
|
||||||
return ParseOutput(instance);
|
return ParseOutput(instance);
|
||||||
}
|
}
|
||||||
|
public static FFProbeFrames GetFrames(string filePath, int outputCapacity = int.MaxValue, 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));
|
||||||
|
|
||||||
|
return ParseFramesOutput(instance);
|
||||||
|
}
|
||||||
public static IMediaAnalysis Analyse(Uri uri, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null)
|
public static IMediaAnalysis Analyse(Uri uri, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null)
|
||||||
{
|
{
|
||||||
using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
|
using var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
|
||||||
var exitCode = instance.BlockUntilFinished();
|
var exitCode = instance.BlockUntilFinished();
|
||||||
if (exitCode != 0)
|
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));
|
throw new FFMpegException(FFMpegExceptionType.Process, $"ffprobe exited with non-zero exit-code ({exitCode} - {string.Join("\n", instance.ErrorData)})", null, string.Join("\n", instance.ErrorData));
|
||||||
|
@ -38,7 +50,7 @@ public static IMediaAnalysis Analyse(Stream stream, int outputCapacity = int.Max
|
||||||
{
|
{
|
||||||
var streamPipeSource = new StreamPipeSource(stream);
|
var streamPipeSource = new StreamPipeSource(stream);
|
||||||
var pipeArgument = new InputPipeArgument(streamPipeSource);
|
var pipeArgument = new InputPipeArgument(streamPipeSource);
|
||||||
using var instance = PrepareInstance(pipeArgument.PipePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
|
using var instance = PrepareStreamAnalysisInstance(pipeArgument.PipePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
|
||||||
pipeArgument.Pre();
|
pipeArgument.Pre();
|
||||||
|
|
||||||
var task = instance.FinishedRunning();
|
var task = instance.FinishedRunning();
|
||||||
|
@ -62,21 +74,37 @@ public static async Task<IMediaAnalysis> AnalyseAsync(string filePath, int outpu
|
||||||
if (!File.Exists(filePath))
|
if (!File.Exists(filePath))
|
||||||
throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'");
|
throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'");
|
||||||
|
|
||||||
using var instance = PrepareInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
|
using var instance = PrepareStreamAnalysisInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
|
||||||
await instance.FinishedRunning().ConfigureAwait(false);
|
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));
|
||||||
|
|
||||||
return ParseOutput(instance);
|
return ParseOutput(instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<FFProbeFrames> GetFramesAsync(string filePath, int outputCapacity = int.MaxValue, 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);
|
||||||
|
}
|
||||||
public static async Task<IMediaAnalysis> AnalyseAsync(Uri uri, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null)
|
public static async Task<IMediaAnalysis> AnalyseAsync(Uri uri, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null)
|
||||||
{
|
{
|
||||||
using var instance = PrepareInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
|
using var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
|
||||||
await instance.FinishedRunning().ConfigureAwait(false);
|
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));
|
||||||
|
|
||||||
return ParseOutput(instance);
|
return ParseOutput(instance);
|
||||||
}
|
}
|
||||||
public static async Task<IMediaAnalysis> AnalyseAsync(Stream stream, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null)
|
public static async Task<IMediaAnalysis> AnalyseAsync(Stream stream, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null)
|
||||||
{
|
{
|
||||||
var streamPipeSource = new StreamPipeSource(stream);
|
var streamPipeSource = new StreamPipeSource(stream);
|
||||||
var pipeArgument = new InputPipeArgument(streamPipeSource);
|
var pipeArgument = new InputPipeArgument(streamPipeSource);
|
||||||
using var instance = PrepareInstance(pipeArgument.PipePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
|
using var instance = PrepareStreamAnalysisInstance(pipeArgument.PipePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current);
|
||||||
pipeArgument.Pre();
|
pipeArgument.Pre();
|
||||||
|
|
||||||
var task = instance.FinishedRunning();
|
var task = instance.FinishedRunning();
|
||||||
|
@ -112,16 +140,33 @@ private static IMediaAnalysis ParseOutput(Instance instance)
|
||||||
|
|
||||||
return new MediaAnalysis(ffprobeAnalysis);
|
return new MediaAnalysis(ffprobeAnalysis);
|
||||||
}
|
}
|
||||||
|
private static FFProbeFrames ParseFramesOutput(Instance instance)
|
||||||
|
{
|
||||||
|
var json = string.Join(string.Empty, instance.OutputData);
|
||||||
|
var ffprobeAnalysis = JsonSerializer.Deserialize<FFProbeFrames>(json, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString | System.Text.Json.Serialization.JsonNumberHandling.WriteAsString
|
||||||
|
}) ;
|
||||||
|
|
||||||
private static Instance PrepareInstance(string filePath, int outputCapacity, FFOptions ffOptions)
|
return ffprobeAnalysis;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 PrepareInstance(string arguments, int outputCapacity, FFOptions ffOptions)
|
||||||
{
|
{
|
||||||
FFProbeHelper.RootExceptionCheck();
|
FFProbeHelper.RootExceptionCheck();
|
||||||
FFProbeHelper.VerifyFFProbeExists(ffOptions);
|
FFProbeHelper.VerifyFFProbeExists(ffOptions);
|
||||||
var arguments = $"-loglevel error -print_format json -show_format -sexagesimal -show_streams \"{filePath}\"";
|
|
||||||
var startInfo = new ProcessStartInfo(GlobalFFOptions.GetFFProbeBinaryPath(), arguments)
|
var startInfo = new ProcessStartInfo(GlobalFFOptions.GetFFProbeBinaryPath(), arguments)
|
||||||
{
|
{
|
||||||
StandardOutputEncoding = ffOptions.Encoding,
|
StandardOutputEncoding = ffOptions.Encoding,
|
||||||
StandardErrorEncoding = ffOptions.Encoding
|
StandardErrorEncoding = ffOptions.Encoding,
|
||||||
|
WorkingDirectory = ffOptions.WorkingDirectory
|
||||||
};
|
};
|
||||||
var instance = new Instance(startInfo) { DataBufferCapacity = outputCapacity };
|
var instance = new Instance(startInfo) { DataBufferCapacity = outputCapacity };
|
||||||
return instance;
|
return instance;
|
||||||
|
|
|
@ -12,7 +12,7 @@ public class FFProbeAnalysis
|
||||||
public Format Format { get; set; } = null!;
|
public Format Format { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class FFProbeStream : ITagsContainer
|
public class FFProbeStream : ITagsContainer, IDispositionContainer
|
||||||
{
|
{
|
||||||
[JsonPropertyName("index")]
|
[JsonPropertyName("index")]
|
||||||
public int Index { get; set; }
|
public int Index { get; set; }
|
||||||
|
@ -71,9 +71,13 @@ public class FFProbeStream : ITagsContainer
|
||||||
[JsonPropertyName("sample_rate")]
|
[JsonPropertyName("sample_rate")]
|
||||||
public string SampleRate { get; set; } = null!;
|
public string SampleRate { get; set; } = null!;
|
||||||
|
|
||||||
|
[JsonPropertyName("disposition")]
|
||||||
|
public Dictionary<string, int> Disposition { get; set; } = null!;
|
||||||
|
|
||||||
[JsonPropertyName("tags")]
|
[JsonPropertyName("tags")]
|
||||||
public Dictionary<string, string> Tags { get; set; } = null!;
|
public Dictionary<string, string> Tags { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Format : ITagsContainer
|
public class Format : ITagsContainer
|
||||||
{
|
{
|
||||||
[JsonPropertyName("filename")]
|
[JsonPropertyName("filename")]
|
||||||
|
@ -110,10 +114,16 @@ public class Format : ITagsContainer
|
||||||
public Dictionary<string, string> Tags { get; set; } = null!;
|
public Dictionary<string, string> Tags { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface IDispositionContainer
|
||||||
|
{
|
||||||
|
Dictionary<string, int> Disposition { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public interface ITagsContainer
|
public interface ITagsContainer
|
||||||
{
|
{
|
||||||
Dictionary<string, string> Tags { get; set; }
|
Dictionary<string, string> Tags { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class TagExtensions
|
public static class TagExtensions
|
||||||
{
|
{
|
||||||
private static string? TryGetTagValue(ITagsContainer tagsContainer, string key)
|
private static string? TryGetTagValue(ITagsContainer tagsContainer, string key)
|
||||||
|
@ -127,7 +137,18 @@ public static class TagExtensions
|
||||||
public static string? GetCreationTime(this ITagsContainer tagsContainer) => TryGetTagValue(tagsContainer, "creation_time ");
|
public static string? GetCreationTime(this ITagsContainer tagsContainer) => TryGetTagValue(tagsContainer, "creation_time ");
|
||||||
public static string? GetRotate(this ITagsContainer tagsContainer) => TryGetTagValue(tagsContainer, "rotate");
|
public static string? GetRotate(this ITagsContainer tagsContainer) => TryGetTagValue(tagsContainer, "rotate");
|
||||||
public static string? GetDuration(this ITagsContainer tagsContainer) => TryGetTagValue(tagsContainer, "duration");
|
public static string? GetDuration(this ITagsContainer tagsContainer) => TryGetTagValue(tagsContainer, "duration");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class DispositionExtensions
|
||||||
|
{
|
||||||
|
private static int? TryGetDispositionValue(IDispositionContainer dispositionContainer, string key)
|
||||||
|
{
|
||||||
|
if (dispositionContainer.Disposition != null && dispositionContainer.Disposition.TryGetValue(key, out var dispositionValue))
|
||||||
|
return dispositionValue;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int? GetDefault(this IDispositionContainer tagsContainer) => TryGetDispositionValue(tagsContainer, "default");
|
||||||
|
public static int? GetForced(this IDispositionContainer tagsContainer) => TryGetDispositionValue(tagsContainer, "forced");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
83
FFMpegCore/FFProbe/FrameAnalysis.cs
Normal file
83
FFMpegCore/FFProbe/FrameAnalysis.cs
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace FFMpegCore
|
||||||
|
{
|
||||||
|
public class FFProbeFrameAnalysis
|
||||||
|
{
|
||||||
|
[JsonPropertyName("media_type")]
|
||||||
|
public string MediaType { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("stream_index")]
|
||||||
|
public int StreamIndex { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("key_frame")]
|
||||||
|
public int KeyFrame { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("pkt_pts")]
|
||||||
|
public long PacketPts { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("pkt_pts_time")]
|
||||||
|
public string PacketPtsTime { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("pkt_dts")]
|
||||||
|
public long PacketDts { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("pkt_dts_time")]
|
||||||
|
public string PacketDtsTime { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("best_effort_timestamp")]
|
||||||
|
public long BestEffortTimestamp { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("best_effort_timestamp_time")]
|
||||||
|
public string BestEffortTimestampTime { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("pkt_duration")]
|
||||||
|
public int PacketDuration { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("pkt_duration_time")]
|
||||||
|
public string PacketDurationTime { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("pkt_pos")]
|
||||||
|
public long PacketPos { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("pkt_size")]
|
||||||
|
public int PacketSize { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("width")]
|
||||||
|
public long Width { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("height")]
|
||||||
|
public long Height { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("pix_fmt")]
|
||||||
|
public string PixelFormat { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("pict_type")]
|
||||||
|
public string PictureType { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("coded_picture_number")]
|
||||||
|
public long CodedPictureNumber { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("display_picture_number")]
|
||||||
|
public long DisplayPictureNumber { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("interlaced_frame")]
|
||||||
|
public int InterlacedFrame { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("top_field_first")]
|
||||||
|
public int TopFieldFirst { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("repeat_pict")]
|
||||||
|
public int RepeatPicture { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("chroma_location")]
|
||||||
|
public string ChromaLocation { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FFProbeFrames
|
||||||
|
{
|
||||||
|
[JsonPropertyName("frames")]
|
||||||
|
public List<FFProbeFrameAnalysis> Frames { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -52,7 +52,7 @@ private VideoStream ParseVideoStream(FFProbeStream stream)
|
||||||
{
|
{
|
||||||
Index = stream.Index,
|
Index = stream.Index,
|
||||||
AvgFrameRate = MediaAnalysisUtils.DivideRatio(MediaAnalysisUtils.ParseRatioDouble(stream.AvgFrameRate, '/')),
|
AvgFrameRate = MediaAnalysisUtils.DivideRatio(MediaAnalysisUtils.ParseRatioDouble(stream.AvgFrameRate, '/')),
|
||||||
BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseIntInvariant(stream.BitRate) : default,
|
BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseLongInvariant(stream.BitRate) : default,
|
||||||
BitsPerRawSample = !string.IsNullOrEmpty(stream.BitsPerRawSample) ? MediaAnalysisUtils.ParseIntInvariant(stream.BitsPerRawSample) : default,
|
BitsPerRawSample = !string.IsNullOrEmpty(stream.BitsPerRawSample) ? MediaAnalysisUtils.ParseIntInvariant(stream.BitsPerRawSample) : default,
|
||||||
CodecName = stream.CodecName,
|
CodecName = stream.CodecName,
|
||||||
CodecLongName = stream.CodecLongName,
|
CodecLongName = stream.CodecLongName,
|
||||||
|
@ -67,6 +67,7 @@ private VideoStream ParseVideoStream(FFProbeStream stream)
|
||||||
PixelFormat = stream.PixelFormat,
|
PixelFormat = stream.PixelFormat,
|
||||||
Rotation = (int)float.Parse(stream.GetRotate() ?? "0"),
|
Rotation = (int)float.Parse(stream.GetRotate() ?? "0"),
|
||||||
Language = stream.GetLanguage(),
|
Language = stream.GetLanguage(),
|
||||||
|
Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition),
|
||||||
Tags = stream.Tags,
|
Tags = stream.Tags,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -76,7 +77,7 @@ private AudioStream ParseAudioStream(FFProbeStream stream)
|
||||||
return new AudioStream
|
return new AudioStream
|
||||||
{
|
{
|
||||||
Index = stream.Index,
|
Index = stream.Index,
|
||||||
BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseIntInvariant(stream.BitRate) : default,
|
BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseLongInvariant(stream.BitRate) : default,
|
||||||
CodecName = stream.CodecName,
|
CodecName = stream.CodecName,
|
||||||
CodecLongName = stream.CodecLongName,
|
CodecLongName = stream.CodecLongName,
|
||||||
CodecTag = stream.CodecTag,
|
CodecTag = stream.CodecTag,
|
||||||
|
@ -87,6 +88,7 @@ private AudioStream ParseAudioStream(FFProbeStream stream)
|
||||||
SampleRateHz = !string.IsNullOrEmpty(stream.SampleRate) ? MediaAnalysisUtils.ParseIntInvariant(stream.SampleRate) : default,
|
SampleRateHz = !string.IsNullOrEmpty(stream.SampleRate) ? MediaAnalysisUtils.ParseIntInvariant(stream.SampleRate) : default,
|
||||||
Profile = stream.Profile,
|
Profile = stream.Profile,
|
||||||
Language = stream.GetLanguage(),
|
Language = stream.GetLanguage(),
|
||||||
|
Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition),
|
||||||
Tags = stream.Tags,
|
Tags = stream.Tags,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -96,11 +98,12 @@ private SubtitleStream ParseSubtitleStream(FFProbeStream stream)
|
||||||
return new SubtitleStream
|
return new SubtitleStream
|
||||||
{
|
{
|
||||||
Index = stream.Index,
|
Index = stream.Index,
|
||||||
BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseIntInvariant(stream.BitRate) : default,
|
BitRate = !string.IsNullOrEmpty(stream.BitRate) ? MediaAnalysisUtils.ParseLongInvariant(stream.BitRate) : default,
|
||||||
CodecName = stream.CodecName,
|
CodecName = stream.CodecName,
|
||||||
CodecLongName = stream.CodecLongName,
|
CodecLongName = stream.CodecLongName,
|
||||||
Duration = MediaAnalysisUtils.ParseDuration(stream),
|
Duration = MediaAnalysisUtils.ParseDuration(stream),
|
||||||
Language = stream.GetLanguage(),
|
Language = stream.GetLanguage(),
|
||||||
|
Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition),
|
||||||
Tags = stream.Tags,
|
Tags = stream.Tags,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -132,6 +135,9 @@ public static double ParseDoubleInvariant(string line) =>
|
||||||
public static int ParseIntInvariant(string line) =>
|
public static int ParseIntInvariant(string line) =>
|
||||||
int.Parse(line, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
|
int.Parse(line, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
public static long ParseLongInvariant(string line) =>
|
||||||
|
long.Parse(line, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
|
||||||
public static TimeSpan ParseDuration(string duration)
|
public static TimeSpan ParseDuration(string duration)
|
||||||
{
|
{
|
||||||
|
@ -169,5 +175,30 @@ public static TimeSpan ParseDuration(FFProbeStream ffProbeStream)
|
||||||
{
|
{
|
||||||
return ParseDuration(ffProbeStream.Duration);
|
return ParseDuration(ffProbeStream.Duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Dictionary<string, bool>? FormatDisposition(Dictionary<string, int>? disposition)
|
||||||
|
{
|
||||||
|
if (disposition == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new Dictionary<string, bool>(disposition.Count);
|
||||||
|
|
||||||
|
foreach (var pair in disposition)
|
||||||
|
{
|
||||||
|
result.Add(pair.Key, ToBool(pair.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ToBool(int value) => value switch
|
||||||
|
{
|
||||||
|
0 => false,
|
||||||
|
1 => true,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(value),
|
||||||
|
$"Not expected disposition state value: {value}")
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -12,9 +12,10 @@ public class MediaStream
|
||||||
public string CodecLongName { get; internal set; } = null!;
|
public string CodecLongName { get; internal set; } = null!;
|
||||||
public string CodecTagString { get; set; } = null!;
|
public string CodecTagString { get; set; } = null!;
|
||||||
public string CodecTag { get; set; } = null!;
|
public string CodecTag { get; set; } = null!;
|
||||||
public int BitRate { get; internal set; }
|
public long BitRate { get; internal set; }
|
||||||
public TimeSpan Duration { get; internal set; }
|
public TimeSpan Duration { get; internal set; }
|
||||||
public string? Language { get; internal set; }
|
public string? Language { get; internal set; }
|
||||||
|
public Dictionary<string, bool>? Disposition { get; internal set; }
|
||||||
public Dictionary<string, string>? Tags { get; internal set; }
|
public Dictionary<string, string>? Tags { get; internal set; }
|
||||||
|
|
||||||
public Codec GetCodecInfo() => FFMpeg.GetCodec(CodecName);
|
public Codec GetCodecInfo() => FFMpeg.GetCodec(CodecName);
|
||||||
|
|
10
README.md
10
README.md
|
@ -196,12 +196,12 @@ The root and temp directory for the ffmpeg binaries can be configured via the `f
|
||||||
```
|
```
|
||||||
|
|
||||||
### Supporting both 32 and 64 bit processes
|
### Supporting both 32 and 64 bit processes
|
||||||
If you wish to support multiple client processor architectures, you can do so by creating a folder `x64` and `x86` in the `root` directory.
|
If you wish to support multiple client processor architectures, you can do so by creating two folders, `x64` and `x86`, in the `BinaryFolder` directory.
|
||||||
Both folders should contain the binaries (`ffmpeg.exe` and `ffprobe.exe`) for build for the respective architectures.
|
Both folders should contain the binaries (`ffmpeg.exe` and `ffprobe.exe`) built for the respective architectures.
|
||||||
|
|
||||||
By doing so, the library will attempt to use either `/root/{ARCH}/(ffmpeg|ffprobe).exe`.
|
By doing so, the library will attempt to use either `/{BinaryFolder}/{ARCH}/(ffmpeg|ffprobe).exe`.
|
||||||
|
|
||||||
If these folders are not defined, it will try to find the binaries in `/root/(ffmpeg|ffprobe.exe)`.
|
If these folders are not defined, it will try to find the binaries in `/{BinaryFolder}/(ffmpeg|ffprobe.exe)`.
|
||||||
|
|
||||||
(`.exe` is only appended on Windows)
|
(`.exe` is only appended on Windows)
|
||||||
|
|
||||||
|
@ -215,7 +215,7 @@ Older versions of ffmpeg might not support all ffmpeg arguments available throug
|
||||||
<img src="https://contrib.rocks/image?repo=rosenbjerg/ffmpegcore" />
|
<img src="https://contrib.rocks/image?repo=rosenbjerg/ffmpegcore" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
## Non-code contributors
|
## Other contributors
|
||||||
<a href="https://github.com/tiesont"><img src="https://avatars3.githubusercontent.com/u/420293?v=4" title="tiesont" width="80" height="80"></a>
|
<a href="https://github.com/tiesont"><img src="https://avatars3.githubusercontent.com/u/420293?v=4" title="tiesont" width="80" height="80"></a>
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue