Merge branch 'master' into patch-3

Former-commit-id: b8298d90b3
This commit is contained in:
Malte Rosenbjerg 2022-04-05 21:35:58 +02:00 committed by GitHub
commit c35372f5d9
31 changed files with 485 additions and 233 deletions

View file

@ -4,10 +4,18 @@ on:
push: push:
branches: branches:
- master - master
paths:
- .github/workflows/ci.yml
- FFMpegCore/**
- FFMpegCore.Test/**
pull_request: pull_request:
branches: branches:
- master - master
- release - release
paths:
- .github/workflows/ci.yml
- FFMpegCore/**
- FFMpegCore.Test/**
jobs: jobs:
ci: ci:
@ -22,7 +30,7 @@ jobs:
- name: Prepare .NET - name: Prepare .NET
uses: actions/setup-dotnet@v1 uses: actions/setup-dotnet@v1
with: with:
dotnet-version: '5.0.x' dotnet-version: '6.0.x'
- name: Prepare FFMpeg - name: Prepare FFMpeg
uses: FedericoCarboni/setup-ffmpeg@v1 uses: FedericoCarboni/setup-ffmpeg@v1
- name: Test with dotnet - name: Test with dotnet

View file

@ -12,7 +12,7 @@ jobs:
- name: Prepare .NET - name: Prepare .NET
uses: actions/setup-dotnet@v1 uses: actions/setup-dotnet@v1
with: with:
dotnet-version: '5.0.x' dotnet-version: '6.0.x'
- name: Build solution - name: Build solution
run: dotnet build --output build -c Release run: dotnet build --output build -c Release
- name: Publish NuGet package - name: Publish NuGet package

View file

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View file

@ -334,7 +334,7 @@ public void Builder_BuildString_SubtitleHardBurnFilter()
.HardBurnSubtitle(SubtitleHardBurnOptions .HardBurnSubtitle(SubtitleHardBurnOptions
.Create(subtitlePath: "sample.srt") .Create(subtitlePath: "sample.srt")
.SetCharacterEncoding("UTF-8") .SetCharacterEncoding("UTF-8")
.SetOriginalSize(1366,768) .SetOriginalSize(1366, 768)
.SetSubtitleIndex(0) .SetSubtitleIndex(0)
.WithStyle(StyleOptions.Create() .WithStyle(StyleOptions.Create()
.WithParameter("FontName", "DejaVu Serif") .WithParameter("FontName", "DejaVu Serif")
@ -479,10 +479,21 @@ public void Builder_BuildString_DynamicAudioNormalizerWithValuesFormat()
{ {
var str = FFMpegArguments.FromFileInput("input.mp4") var str = FFMpegArguments.FromFileInput("input.mp4")
.OutputToFile("output.mp4", false, .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; .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); 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);
}
} }
} }

View file

@ -94,5 +94,20 @@ public void Concat_Escape()
var arg = new DemuxConcatArgument(new[] { @"Heaven's River\05 - Investigation.m4b" }); var arg = new DemuxConcatArgument(new[] { @"Heaven's River\05 - Investigation.m4b" });
arg.Values.Should().BeEquivalentTo(new[] { @"file 'Heaven'\''s River\05 - Investigation.m4b'" }); arg.Values.Should().BeEquivalentTo(new[] { @"file 'Heaven'\''s River\05 - Investigation.m4b'" });
} }
[TestMethod]
public void Audible_Aaxc_Test()
{
var arg = new AudibleEncryptionKeyArgument("123", "456");
arg.Text.Should().Be($"-audible_key 123 -audible_iv 456");
}
[TestMethod]
public void Audible_Aax_Test()
{
var arg = new AudibleEncryptionKeyArgument("62689101");
arg.Text.Should().Be($"-activation_bytes 62689101");
}
} }
} }

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net5.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>

View file

@ -11,13 +11,6 @@ namespace FFMpegCore.Test
[TestClass] [TestClass]
public class FFProbeTests public class FFProbeTests
{ {
[TestMethod]
public void Probe_TooLongOutput()
{
Assert.ThrowsException<System.Text.Json.JsonException>(() => FFProbe.Analyse(TestResources.Mp4Video, 5));
}
[TestMethod] [TestMethod]
public async Task Audio_FromStream_Duration() public async Task Audio_FromStream_Duration()
{ {

View file

@ -4,8 +4,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace FFMpegCore.Test namespace FFMpegCore.Test
@ -50,5 +52,29 @@ public void TestMetaDataBuilderIntegrity()
Assert.IsTrue(serialized.Contains("title=Chapter 01", StringComparison.OrdinalIgnoreCase)); Assert.IsTrue(serialized.Contains("title=Chapter 01", StringComparison.OrdinalIgnoreCase));
Assert.IsTrue(serialized.Contains("album_artist=Pachelbel", 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.");
}
} }
} }

View file

@ -544,7 +544,7 @@ public void Video_OutputsData()
.WithVerbosityLevel(VerbosityLevel.Info)) .WithVerbosityLevel(VerbosityLevel.Info))
.OutputToFile(outputFile, false, opt => opt .OutputToFile(outputFile, false, opt => opt
.WithDuration(TimeSpan.FromSeconds(2))) .WithDuration(TimeSpan.FromSeconds(2)))
.NotifyOnOutput((_, _) => dataReceived = true) .NotifyOnError(_ => dataReceived = true)
.ProcessSynchronously(); .ProcessSynchronously();
Assert.IsTrue(dataReceived); Assert.IsTrue(dataReceived);

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Text; using System.Text;
namespace FFMpegCore.Extend namespace FFMpegCore.Extend

View file

@ -0,0 +1,28 @@
namespace FFMpegCore.Arguments
{
public class AudibleEncryptionKeyArgument : IArgument
{
private readonly bool _aaxcMode;
private readonly string _key;
private readonly string _iv;
private readonly string _activationBytes;
public AudibleEncryptionKeyArgument(string activationBytes)
{
_activationBytes = activationBytes;
}
public AudibleEncryptionKeyArgument(string key, string iv)
{
_aaxcMode = true;
_key = key;
_iv = iv;
}
public string Text => _aaxcMode ? $"-audible_key {_key} -audible_iv {_iv}" : $"-activation_bytes {_activationBytes}";
}
}

View file

@ -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}";
}
}

View file

@ -0,0 +1,16 @@
using System.Collections.Generic;
using System.Text;
namespace FFMpegCore.Arguments
{
public interface IDynamicArgument
{
/// <summary>
/// Same as <see cref="IArgument.Text"/>, but this receives the arguments generated before as parameter
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
//public string GetText(StringBuilder context);
public string GetText(IEnumerable<IArgument> context);
}
}

View file

@ -0,0 +1,64 @@
using FFMpegCore.Extend;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace FFMpegCore.Arguments
{
public class MapMetadataArgument : IInputArgument, IDynamicArgument
{
private readonly int? _inputIndex;
public string Text => GetText(null);
/// <summary>
/// Null means it takes the last input used before this argument
/// </summary>
/// <param name="inputIndex"></param>
public MapMetadataArgument(int? inputIndex = null)
{
_inputIndex = inputIndex;
}
public string GetText(IEnumerable<IArgument>? arguments)
{
arguments ??= Enumerable.Empty<IArgument>();
var index = 0;
if (_inputIndex is null)
{
index = arguments
.TakeWhile(x => x != this)
.OfType<IInputArgument>()
.Count();
index = Math.Max(index - 1, 0);
}
else
{
index = _inputIndex.Value;
}
return $"-map_metadata {index}";
}
public Task During(CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public void Post()
{
}
public void Pre()
{
}
}
}

View file

@ -1,11 +1,16 @@
using System; using FFMpegCore.Extend;
using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace FFMpegCore.Arguments namespace FFMpegCore.Arguments
{ {
public class MetaDataArgument : IInputArgument public class MetaDataArgument : IInputArgument, IDynamicArgument
{ {
private readonly string _metaDataContent; private readonly string _metaDataContent;
private readonly string _tempFileName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, $"metadata_{Guid.NewGuid()}.txt"); private readonly string _tempFileName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, $"metadata_{Guid.NewGuid()}.txt");
@ -15,7 +20,7 @@ public MetaDataArgument(string metaDataContent)
_metaDataContent = metaDataContent; _metaDataContent = metaDataContent;
} }
public string Text => $"-i \"{_tempFileName}\" -map_metadata 1"; public string Text => GetText(null);
public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask; public Task During(CancellationToken cancellationToken = default) => Task.CompletedTask;
@ -23,5 +28,17 @@ public MetaDataArgument(string metaDataContent)
public void Pre() => File.WriteAllText(_tempFileName, _metaDataContent); public void Pre() => File.WriteAllText(_tempFileName, _metaDataContent);
public void Post() => File.Delete(_tempFileName); public void Post() => File.Delete(_tempFileName);
public string GetText(IEnumerable<IArgument>? arguments)
{
arguments ??= Enumerable.Empty<IArgument>();
var index = arguments
.TakeWhile(x => x != this)
.OfType<IInputArgument>()
.Count();
return $"-i \"{_tempFileName}\" -map_metadata {index}";
}
} }
} }

View file

@ -8,6 +8,7 @@
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Instances;
namespace FFMpegCore namespace FFMpegCore
{ {
@ -246,7 +247,10 @@ public static bool Convert(
public static bool PosterWithAudio(string image, string audio, string output) public static bool PosterWithAudio(string image, string audio, string output)
{ {
FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.Mp4); FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.Mp4);
FFMpegHelper.ConversionSizeExceptionCheck(Image.FromFile(image)); using (var imageFile = Image.FromFile(image))
{
FFMpegHelper.ConversionSizeExceptionCheck(imageFile);
}
return FFMpegArguments return FFMpegArguments
.FromFileInput(image, false, options => options .FromFileInput(image, false, options => options
@ -417,15 +421,16 @@ internal static IReadOnlyList<PixelFormat> GetPixelFormatsInternal()
FFMpegHelper.RootExceptionCheck(); FFMpegHelper.RootExceptionCheck();
var list = new List<PixelFormat>(); var list = new List<PixelFormat>();
using var instance = new Instances.Instance(GlobalFFOptions.GetFFMpegBinaryPath(), "-pix_fmts"); var processArguments = new ProcessArguments(GlobalFFOptions.GetFFMpegBinaryPath(), "-pix_fmts");
instance.DataReceived += (e, args) => processArguments.OutputDataReceived += (e, data) =>
{ {
if (PixelFormat.TryParse(args.Data, out var format)) if (PixelFormat.TryParse(data, out var format))
list.Add(format); list.Add(format);
}; };
var exitCode = instance.BlockUntilFinished(); var result = processArguments.StartAndWaitForExit();
if (exitCode != 0) throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\r\n", instance.OutputData)); if (result.ExitCode != 0)
throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\r\n", result.OutputData));
return list.AsReadOnly(); return list.AsReadOnly();
} }
@ -462,10 +467,10 @@ private static void ParsePartOfCodecs(Dictionary<string, Codec> codecs, string a
{ {
FFMpegHelper.RootExceptionCheck(); FFMpegHelper.RootExceptionCheck();
using var instance = new Instances.Instance(GlobalFFOptions.GetFFMpegBinaryPath(), arguments); var processArguments = new ProcessArguments(GlobalFFOptions.GetFFMpegBinaryPath(), arguments);
instance.DataReceived += (e, args) => processArguments.OutputDataReceived += (e, data) =>
{ {
var codec = parser(args.Data); var codec = parser(data);
if(codec != null) if(codec != null)
if (codecs.TryGetValue(codec.Name, out var parentCodec)) if (codecs.TryGetValue(codec.Name, out var parentCodec))
parentCodec.Merge(codec); parentCodec.Merge(codec);
@ -473,8 +478,8 @@ private static void ParsePartOfCodecs(Dictionary<string, Codec> codecs, string a
codecs.Add(codec.Name, codec); codecs.Add(codec.Name, codec);
}; };
var exitCode = instance.BlockUntilFinished(); var result = processArguments.StartAndWaitForExit();
if (exitCode != 0) throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\r\n", instance.OutputData)); if (result.ExitCode != 0) throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\r\n", result.OutputData));
} }
internal static Dictionary<string, Codec> GetCodecsInternal() internal static Dictionary<string, Codec> GetCodecsInternal()
@ -546,15 +551,15 @@ internal static IReadOnlyList<ContainerFormat> GetContainersFormatsInternal()
FFMpegHelper.RootExceptionCheck(); FFMpegHelper.RootExceptionCheck();
var list = new List<ContainerFormat>(); var list = new List<ContainerFormat>();
using var instance = new Instances.Instance(GlobalFFOptions.GetFFMpegBinaryPath(), "-formats"); var instance = new ProcessArguments(GlobalFFOptions.GetFFMpegBinaryPath(), "-formats");
instance.DataReceived += (e, args) => instance.OutputDataReceived += (e, data) =>
{ {
if (ContainerFormat.TryParse(args.Data, out var fmt)) if (ContainerFormat.TryParse(data, out var fmt))
list.Add(fmt); list.Add(fmt);
}; };
var exitCode = instance.BlockUntilFinished(); var result = instance.StartAndWaitForExit();
if (exitCode != 0) throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\r\n", instance.OutputData)); if (result.ExitCode != 0) throw new FFMpegException(FFMpegExceptionType.Process, string.Join("\r\n", result.OutputData));
return list.AsReadOnly(); return list.AsReadOnly();
} }

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Drawing; using System.Drawing;
using FFMpegCore.Arguments; using FFMpegCore.Arguments;
using FFMpegCore.Enums; using FFMpegCore.Enums;
@ -66,6 +67,11 @@ public FFMpegArgumentOptions WithAudioFilters(Action<AudioFilterOptions> audioFi
public FFMpegArgumentOptions ForcePixelFormat(string pixelFormat) => WithArgument(new ForcePixelFormat(pixelFormat)); public FFMpegArgumentOptions ForcePixelFormat(string pixelFormat) => WithArgument(new ForcePixelFormat(pixelFormat));
public FFMpegArgumentOptions ForcePixelFormat(PixelFormat pixelFormat) => WithArgument(new ForcePixelFormat(pixelFormat)); public FFMpegArgumentOptions ForcePixelFormat(PixelFormat pixelFormat) => WithArgument(new ForcePixelFormat(pixelFormat));
public FFMpegArgumentOptions WithAudibleEncryptionKeys(string key, string iv) => WithArgument(new AudibleEncryptionKeyArgument(key, iv));
public FFMpegArgumentOptions WithAudibleActivationBytes(string activationBytes) => WithArgument(new AudibleEncryptionKeyArgument(activationBytes));
public FFMpegArgumentOptions WithTagVersion(int id3v2Version = 3) => WithArgument(new ID3V2VersionArgument(id3v2Version));
public FFMpegArgumentOptions WithArgument(IArgument argument) public FFMpegArgumentOptions WithArgument(IArgument argument)
{ {
Arguments.Add(argument); Arguments.Add(argument);

View file

@ -18,7 +18,8 @@ public class FFMpegArgumentProcessor
private readonly FFMpegArguments _ffMpegArguments; private readonly FFMpegArguments _ffMpegArguments;
private Action<double>? _onPercentageProgress; private Action<double>? _onPercentageProgress;
private Action<TimeSpan>? _onTimeProgress; private Action<TimeSpan>? _onTimeProgress;
private Action<string, DataType>? _onOutput; private Action<string>? _onOutput;
private Action<string>? _onError;
private TimeSpan? _totalTimespan; private TimeSpan? _totalTimespan;
internal FFMpegArgumentProcessor(FFMpegArguments ffMpegArguments) internal FFMpegArgumentProcessor(FFMpegArguments ffMpegArguments)
@ -57,11 +58,16 @@ public FFMpegArgumentProcessor NotifyOnProgress(Action<TimeSpan> onTimeProgress)
/// Register action that will be invoked during the ffmpeg processing, when a line is output /// Register action that will be invoked during the ffmpeg processing, when a line is output
/// </summary> /// </summary>
/// <param name="onOutput"></param> /// <param name="onOutput"></param>
public FFMpegArgumentProcessor NotifyOnOutput(Action<string, DataType> onOutput) public FFMpegArgumentProcessor NotifyOnOutput(Action<string> onOutput)
{ {
_onOutput = onOutput; _onOutput = onOutput;
return this; return this;
} }
public FFMpegArgumentProcessor NotifyOnError(Action<string> onError)
{
_onError = onError;
return this;
}
public FFMpegArgumentProcessor CancellableThrough(out Action cancel, int timeout = 0) public FFMpegArgumentProcessor CancellableThrough(out Action cancel, int timeout = 0)
{ {
cancel = () => CancelEvent?.Invoke(this, timeout); cancel = () => CancelEvent?.Invoke(this, timeout);
@ -80,43 +86,47 @@ public FFMpegArgumentProcessor Configure(Action<FFOptions> configureOptions)
public bool ProcessSynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null) public bool ProcessSynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null)
{ {
var options = GetConfiguredOptions(ffMpegOptions); 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) IProcessResult? processResult = null;
{
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;
try try
{ {
errorCode = Process(instance, cancellationTokenSource).ConfigureAwait(false).GetAwaiter().GetResult(); processResult = Process(processArguments, cancellationTokenSource).ConfigureAwait(false).GetAwaiter().GetResult();
} }
catch (Exception e) catch (Exception e)
{ {
if (!HandleException(throwOnError, e, instance.ErrorData)) return false; if (!HandleException(throwOnError, e, processResult?.ErrorData ?? Array.Empty<string>())) return false;
}
finally
{
CancelEvent -= OnCancelEvent;
} }
return HandleCompletion(throwOnError, errorCode, instance.ErrorData); return HandleCompletion(throwOnError, processResult?.ExitCode ?? -1, processResult?.ErrorData ?? Array.Empty<string>());
} }
public async Task<bool> ProcessAsynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null) public async Task<bool> ProcessAsynchronously(bool throwOnError = true, FFOptions? ffMpegOptions = null)
{ {
var options = GetConfiguredOptions(ffMpegOptions); 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<string>())) return false;
}
return HandleCompletion(throwOnError, processResult?.ExitCode ?? -1, processResult?.ErrorData ?? Array.Empty<string>());
}
private async Task<IProcessResult> Process(ProcessArguments processArguments, CancellationTokenSource cancellationTokenSource)
{
IProcessResult processResult = null!;
_ffMpegArguments.Pre();
using var instance = processArguments.Start();
void OnCancelEvent(object sender, int timeout) void OnCancelEvent(object sender, int timeout)
{ {
instance.SendInput("q"); instance.SendInput("q");
@ -124,41 +134,26 @@ void OnCancelEvent(object sender, int timeout)
if (!cancellationTokenSource.Token.WaitHandle.WaitOne(timeout, true)) if (!cancellationTokenSource.Token.WaitHandle.WaitOne(timeout, true))
{ {
cancellationTokenSource.Cancel(); cancellationTokenSource.Cancel();
instance.Started = false; instance.Kill();
} }
} }
CancelEvent += OnCancelEvent; CancelEvent += OnCancelEvent;
var errorCode = -1;
try try
{ {
errorCode = await Process(instance, cancellationTokenSource).ConfigureAwait(false); await Task.WhenAll(instance.WaitForExitAsync().ContinueWith(t =>
}
catch (Exception e)
{ {
if (!HandleException(throwOnError, e, instance.ErrorData)) return false; processResult = t.Result;
cancellationTokenSource.Cancel();
_ffMpegArguments.Post();
}), _ffMpegArguments.During(cancellationTokenSource.Token)).ConfigureAwait(false);
return processResult;
} }
finally finally
{ {
CancelEvent -= OnCancelEvent; CancelEvent -= OnCancelEvent;
} }
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)
@ -184,7 +179,7 @@ internal FFOptions GetConfiguredOptions(FFOptions? ffOptions)
return options; return options;
} }
private Instance PrepareInstance(FFOptions ffOptions, private ProcessArguments PrepareProcessArguments(FFOptions ffOptions,
out CancellationTokenSource cancellationTokenSource) out CancellationTokenSource cancellationTokenSource)
{ {
FFMpegHelper.RootExceptionCheck(); FFMpegHelper.RootExceptionCheck();
@ -197,17 +192,25 @@ private Instance PrepareInstance(FFOptions ffOptions,
StandardErrorEncoding = ffOptions.Encoding, StandardErrorEncoding = ffOptions.Encoding,
WorkingDirectory = ffOptions.WorkingDirectory WorkingDirectory = ffOptions.WorkingDirectory
}; };
var instance = new Instance(startInfo); var processArguments = new ProcessArguments(startInfo);
cancellationTokenSource = new CancellationTokenSource(); cancellationTokenSource = new CancellationTokenSource();
if (_onOutput != null || _onTimeProgress != null || (_onPercentageProgress != null && _totalTimespan != null)) if (_onOutput != null || _onTimeProgress != null || (_onPercentageProgress != null && _totalTimespan != null))
instance.DataReceived += OutputData; processArguments.OutputDataReceived += OutputData;
return instance; if (_onError != null)
processArguments.ErrorDataReceived += ErrorData;
return processArguments;
}
private void ErrorData(object sender, string msg)
{
_onError?.Invoke(msg);
} }
private static bool HandleException(bool throwOnError, Exception e, IReadOnlyList<string> errorData) private static bool HandleException(bool throwOnError, Exception e, IEnumerable<string> errorData)
{ {
if (!throwOnError) if (!throwOnError)
return false; 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)); 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); Debug.WriteLine(msg);
_onOutput?.Invoke(msg.Data, msg.Type); _onOutput?.Invoke(msg);
var match = ProgressRegex.Match(msg.Data); var match = ProgressRegex.Match(msg);
if (!match.Success) return; if (!match.Success) return;
var processed = TimeSpan.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); var processed = TimeSpan.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture);

View file

@ -2,8 +2,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using FFMpegCore.Arguments; using FFMpegCore.Arguments;
using FFMpegCore.Builders.MetaData; using FFMpegCore.Builders.MetaData;
using FFMpegCore.Pipes; using FFMpegCore.Pipes;
@ -16,7 +18,13 @@ public sealed class FFMpegArguments : FFMpegArgumentsBase
private FFMpegArguments() { } private FFMpegArguments() { }
public string Text => string.Join(" ", _globalArguments.Arguments.Concat(Arguments).Select(arg => arg.Text)); public string Text => GetText();
private string GetText()
{
var allArguments = _globalArguments.Arguments.Concat(Arguments).ToArray();
return string.Join(" ", allArguments.Select(arg => arg is IDynamicArgument dynArg ? dynArg.GetText(allArguments) : arg.Text));
}
public static FFMpegArguments FromConcatInput(IEnumerable<string> filePaths, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new ConcatArgument(filePaths), addArguments); public static FFMpegArguments FromConcatInput(IEnumerable<string> filePaths, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new ConcatArgument(filePaths), addArguments);
public static FFMpegArguments FromDemuxConcatInput(IEnumerable<string> filePaths, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new DemuxConcatArgument(filePaths), addArguments); public static FFMpegArguments FromDemuxConcatInput(IEnumerable<string> filePaths, Action<FFMpegArgumentOptions>? addArguments = null) => new FFMpegArguments().WithInput(new DemuxConcatArgument(filePaths), addArguments);
@ -42,6 +50,13 @@ public FFMpegArguments WithGlobalOptions(Action<FFMpegGlobalArguments> configure
public FFMpegArguments AddMetaData(string content, Action<FFMpegArgumentOptions>? addArguments = null) => WithInput(new MetaDataArgument(content), addArguments); public FFMpegArguments AddMetaData(string content, Action<FFMpegArgumentOptions>? addArguments = null) => WithInput(new MetaDataArgument(content), addArguments);
public FFMpegArguments AddMetaData(IReadOnlyMetaData metaData, Action<FFMpegArgumentOptions>? addArguments = null) => WithInput(new MetaDataArgument(MetaDataSerializer.Instance.Serialize(metaData)), addArguments); public FFMpegArguments AddMetaData(IReadOnlyMetaData metaData, Action<FFMpegArgumentOptions>? addArguments = null) => WithInput(new MetaDataArgument(MetaDataSerializer.Instance.Serialize(metaData)), addArguments);
/// <summary>
/// Maps the metadata of the given stream
/// </summary>
/// <param name="inputIndex">null means, the previous input will be used</param>
public FFMpegArguments MapMetaData(int? inputIndex = null, Action<FFMpegArgumentOptions>? addArguments = null) => WithInput(new MapMetadataArgument(inputIndex), addArguments);
private FFMpegArguments WithInput(IInputArgument inputArgument, Action<FFMpegArgumentOptions>? addArguments) private FFMpegArguments WithInput(IInputArgument inputArgument, Action<FFMpegArgumentOptions>? addArguments)
{ {
var arguments = new FFMpegArgumentOptions(); var arguments = new FFMpegArgumentOptions();

View file

@ -32,9 +32,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Instances" Version="1.6.1" /> <PackageReference Include="Instances" Version="2.0.0" />
<PackageReference Include="System.Drawing.Common" Version="5.0.2" /> <PackageReference Include="System.Drawing.Common" Version="5.0.2" />
<PackageReference Include="System.Text.Json" Version="5.0.1" /> <PackageReference Include="System.Text.Json" Version="6.0.2" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -2,9 +2,9 @@
{ {
public class AudioStream : MediaStream public class AudioStream : MediaStream
{ {
public int Channels { get; internal set; } public int Channels { get; set; }
public string ChannelLayout { get; internal set; } = null!; public string ChannelLayout { get; set; } = null!;
public int SampleRateHz { get; internal set; } public int SampleRateHz { get; set; }
public string Profile { get; internal set; } = null!; public string Profile { get; set; } = null!;
} }
} }

View file

@ -2,6 +2,7 @@
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Text.Json; using System.Text.Json;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using FFMpegCore.Arguments; using FFMpegCore.Arguments;
using FFMpegCore.Exceptions; using FFMpegCore.Exceptions;
@ -13,61 +14,55 @@ namespace FFMpegCore
{ {
public static class FFProbe 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)) ThrowIfInputFileDoesNotExist(filePath);
throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'");
using var instance = PrepareStreamAnalysisInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); var processArguments = PrepareStreamAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current);
var exitCode = instance.BlockUntilFinished(); var result = processArguments.StartAndWaitForExit();
if (exitCode != 0) ThrowIfExitCodeNotZero(result);
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(result);
}
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 FFProbePackets GetPackets(string filePath, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) public static FFProbeFrames GetFrames(string filePath, FFOptions? ffOptions = null)
{ {
if (!File.Exists(filePath)) ThrowIfInputFileDoesNotExist(filePath);
throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'");
using var instance = PreparePacketAnalysisInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); var instance = PrepareFrameAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current);
var exitCode = instance.BlockUntilFinished(); var result = instance.StartAndWaitForExit();
if (exitCode != 0) ThrowIfExitCodeNotZero(result);
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 ParsePacketsOutput(instance); return ParseFramesOutput(result);
} }
public static IMediaAnalysis Analyse(Uri uri, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) public static FFProbePackets GetPackets(string filePath, FFOptions? ffOptions = null)
{ {
using var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current); ThrowIfInputFileDoesNotExist(filePath);
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 ParseOutput(instance); var instance = PreparePacketAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current);
var result = instance.StartAndWaitForExit();
ThrowIfExitCodeNotZero(result);
return ParsePacketsOutput(result);
} }
public static IMediaAnalysis Analyse(Stream stream, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null)
public static IMediaAnalysis Analyse(Uri uri, FFOptions? ffOptions = null)
{
var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current);
var result = instance.StartAndWaitForExit();
ThrowIfExitCodeNotZero(result);
return ParseOutput(result);
}
public static IMediaAnalysis Analyse(Stream stream, 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 = PrepareStreamAnalysisInstance(pipeArgument.PipePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); var instance = PrepareStreamAnalysisInstance(pipeArgument.PipePath, ffOptions ?? GlobalFFOptions.Current);
pipeArgument.Pre(); pipeArgument.Pre();
var task = instance.FinishedRunning(); var task = instance.StartAndWaitForExitAsync();
try try
{ {
pipeArgument.During().ConfigureAwait(false).GetAwaiter().GetResult(); pipeArgument.During().ConfigureAwait(false).GetAwaiter().GetResult();
@ -77,65 +72,60 @@ public static IMediaAnalysis Analyse(Stream stream, int outputCapacity = int.Max
{ {
pipeArgument.Post(); pipeArgument.Post();
} }
var exitCode = task.ConfigureAwait(false).GetAwaiter().GetResult(); var result = task.ConfigureAwait(false).GetAwaiter().GetResult();
if (exitCode != 0) ThrowIfExitCodeNotZero(result);
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(result);
} }
public static async Task<IMediaAnalysis> AnalyseAsync(string filePath, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null)
public static async Task<IMediaAnalysis> AnalyseAsync(string filePath, FFOptions? ffOptions = null, CancellationToken cancellationToken = default)
{ {
if (!File.Exists(filePath)) ThrowIfInputFileDoesNotExist(filePath);
throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'");
using var instance = PrepareStreamAnalysisInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); var instance = PrepareStreamAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current);
var exitCode = await instance.FinishedRunning().ConfigureAwait(false); var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false);
if (exitCode != 0) ThrowIfExitCodeNotZero(result);
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(result);
} }
public static async Task<FFProbeFrames> GetFramesAsync(string filePath, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) public static async Task<FFProbeFrames> GetFramesAsync(string filePath, FFOptions? ffOptions = null, CancellationToken cancellationToken = default)
{ {
if (!File.Exists(filePath)) ThrowIfInputFileDoesNotExist(filePath);
throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'");
using var instance = PrepareFrameAnalysisInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); var instance = PrepareFrameAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current);
await instance.FinishedRunning().ConfigureAwait(false); var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false);
return ParseFramesOutput(instance); return ParseFramesOutput(result);
} }
public static async Task<FFProbePackets> GetPacketsAsync(string filePath, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) public static async Task<FFProbePackets> GetPacketsAsync(string filePath, FFOptions? ffOptions = null, CancellationToken cancellationToken = default)
{ {
if (!File.Exists(filePath)) ThrowIfInputFileDoesNotExist(filePath);
throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'");
using var instance = PreparePacketAnalysisInstance(filePath, outputCapacity, ffOptions ?? GlobalFFOptions.Current); var instance = PreparePacketAnalysisInstance(filePath, ffOptions ?? GlobalFFOptions.Current);
await instance.FinishedRunning().ConfigureAwait(false); var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false);
return ParsePacketsOutput(instance); return ParsePacketsOutput(result);
} }
public static async Task<IMediaAnalysis> AnalyseAsync(Uri uri, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) public static async Task<IMediaAnalysis> AnalyseAsync(Uri uri, FFOptions? ffOptions = null, CancellationToken cancellationToken = default)
{ {
using var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, outputCapacity, ffOptions ?? GlobalFFOptions.Current); var instance = PrepareStreamAnalysisInstance(uri.AbsoluteUri, ffOptions ?? GlobalFFOptions.Current);
var exitCode = await instance.FinishedRunning().ConfigureAwait(false); var result = await instance.StartAndWaitForExitAsync(cancellationToken).ConfigureAwait(false);
if (exitCode != 0) ThrowIfExitCodeNotZero(result);
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(result);
} }
public static async Task<IMediaAnalysis> AnalyseAsync(Stream stream, int outputCapacity = int.MaxValue, FFOptions? ffOptions = null) public static async Task<IMediaAnalysis> AnalyseAsync(Stream stream, FFOptions? ffOptions = null, CancellationToken cancellationToken = default)
{ {
var streamPipeSource = new StreamPipeSource(stream); var streamPipeSource = new StreamPipeSource(stream);
var pipeArgument = new InputPipeArgument(streamPipeSource); 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(); pipeArgument.Pre();
var task = instance.FinishedRunning(); var task = instance.StartAndWaitForExitAsync(cancellationToken);
try try
{ {
await pipeArgument.During().ConfigureAwait(false); await pipeArgument.During(cancellationToken).ConfigureAwait(false);
} }
catch(IOException) catch(IOException)
{ {
@ -144,15 +134,14 @@ public static async Task<IMediaAnalysis> AnalyseAsync(Stream stream, int outputC
{ {
pipeArgument.Post(); pipeArgument.Post();
} }
var exitCode = await task.ConfigureAwait(false); var result = await task.ConfigureAwait(false);
if (exitCode != 0) ThrowIfExitCodeNotZero(result);
throw new FFProbeProcessException($"ffprobe exited with non-zero exit-code ({exitCode} - {string.Join("\n", instance.ErrorData)})", instance.ErrorData);
pipeArgument.Post(); 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 json = string.Join(string.Empty, instance.OutputData);
var ffprobeAnalysis = JsonSerializer.Deserialize<FFProbeAnalysis>(json, new JsonSerializerOptions var ffprobeAnalysis = JsonSerializer.Deserialize<FFProbeAnalysis>(json, new JsonSerializerOptions
@ -166,7 +155,7 @@ private static IMediaAnalysis ParseOutput(Instance instance)
ffprobeAnalysis.ErrorData = instance.ErrorData; ffprobeAnalysis.ErrorData = instance.ErrorData;
return new MediaAnalysis(ffprobeAnalysis); 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 json = string.Join(string.Empty, instance.OutputData);
var ffprobeAnalysis = JsonSerializer.Deserialize<FFProbeFrames>(json, new JsonSerializerOptions var ffprobeAnalysis = JsonSerializer.Deserialize<FFProbeFrames>(json, new JsonSerializerOptions
@ -175,10 +164,10 @@ private static FFProbeFrames ParseFramesOutput(Instance instance)
NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString | System.Text.Json.Serialization.JsonNumberHandling.WriteAsString NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString | System.Text.Json.Serialization.JsonNumberHandling.WriteAsString
}) ; }) ;
return ffprobeAnalysis; return ffprobeAnalysis!;
} }
private static FFProbePackets ParsePacketsOutput(Instance instance) private static FFProbePackets ParsePacketsOutput(IProcessResult instance)
{ {
var json = string.Join(string.Empty, instance.OutputData); var json = string.Join(string.Empty, instance.OutputData);
var ffprobeAnalysis = JsonSerializer.Deserialize<FFProbePackets>(json, new JsonSerializerOptions var ffprobeAnalysis = JsonSerializer.Deserialize<FFProbePackets>(json, new JsonSerializerOptions
@ -187,29 +176,44 @@ private static FFProbePackets ParsePacketsOutput(Instance instance)
NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString | System.Text.Json.Serialization.JsonNumberHandling.WriteAsString NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString | System.Text.Json.Serialization.JsonNumberHandling.WriteAsString
}) ; }) ;
return ffprobeAnalysis; return ffprobeAnalysis!;
} }
private static void ThrowIfInputFileDoesNotExist(string filePath)
{
if (!File.Exists(filePath))
{
throw new FFMpegException(FFMpegExceptionType.File, $"No file found at '{filePath}'");
}
}
private static Instance PrepareStreamAnalysisInstance(string filePath, int outputCapacity, FFOptions ffOptions) private static void ThrowIfExitCodeNotZero(IProcessResult result)
=> PrepareInstance($"-loglevel error -print_format json -show_format -sexagesimal -show_streams \"{filePath}\"", outputCapacity, ffOptions); {
private static Instance PrepareFrameAnalysisInstance(string filePath, int outputCapacity, FFOptions ffOptions) if (result.ExitCode != 0)
=> PrepareInstance($"-loglevel error -print_format json -show_frames -v quiet -sexagesimal \"{filePath}\"", outputCapacity, ffOptions); {
private static Instance PreparePacketAnalysisInstance(string filePath, int outputCapacity, FFOptions ffOptions) var message = $"ffprobe exited with non-zero exit-code ({result.ExitCode} - {string.Join("\n", result.ErrorData)})";
=> PrepareInstance($"-loglevel error -print_format json -show_packets -v quiet -sexagesimal \"{filePath}\"", outputCapacity, ffOptions); throw new FFMpegException(FFMpegExceptionType.Process, message, null, string.Join("\n", result.ErrorData));
}
}
private static Instance PrepareInstance(string arguments, int outputCapacity, FFOptions 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 ProcessArguments PrepareInstance(string arguments, FFOptions ffOptions)
{ {
FFProbeHelper.RootExceptionCheck(); FFProbeHelper.RootExceptionCheck();
FFProbeHelper.VerifyFFProbeExists(ffOptions); FFProbeHelper.VerifyFFProbeExists(ffOptions);
var startInfo = new ProcessStartInfo(GlobalFFOptions.GetFFProbeBinaryPath(), arguments) var startInfo = new ProcessStartInfo(GlobalFFOptions.GetFFProbeBinaryPath(ffOptions), arguments)
{ {
StandardOutputEncoding = ffOptions.Encoding, StandardOutputEncoding = ffOptions.Encoding,
StandardErrorEncoding = ffOptions.Encoding, StandardErrorEncoding = ffOptions.Encoding,
WorkingDirectory = ffOptions.WorkingDirectory WorkingDirectory = ffOptions.WorkingDirectory
}; };
var instance = new Instance(startInfo) { DataBufferCapacity = outputCapacity }; return new ProcessArguments(startInfo);
return instance;
} }
} }
} }

View file

@ -6,7 +6,7 @@ namespace FFMpegCore
public class FFProbeFrameAnalysis public class FFProbeFrameAnalysis
{ {
[JsonPropertyName("media_type")] [JsonPropertyName("media_type")]
public string MediaType { get; set; } public string MediaType { get; set; } = null!;
[JsonPropertyName("stream_index")] [JsonPropertyName("stream_index")]
public int StreamIndex { get; set; } public int StreamIndex { get; set; }
@ -18,25 +18,25 @@ public class FFProbeFrameAnalysis
public long PacketPts { get; set; } public long PacketPts { get; set; }
[JsonPropertyName("pkt_pts_time")] [JsonPropertyName("pkt_pts_time")]
public string PacketPtsTime { get; set; } public string PacketPtsTime { get; set; } = null!;
[JsonPropertyName("pkt_dts")] [JsonPropertyName("pkt_dts")]
public long PacketDts { get; set; } public long PacketDts { get; set; }
[JsonPropertyName("pkt_dts_time")] [JsonPropertyName("pkt_dts_time")]
public string PacketDtsTime { get; set; } public string PacketDtsTime { get; set; } = null!;
[JsonPropertyName("best_effort_timestamp")] [JsonPropertyName("best_effort_timestamp")]
public long BestEffortTimestamp { get; set; } public long BestEffortTimestamp { get; set; }
[JsonPropertyName("best_effort_timestamp_time")] [JsonPropertyName("best_effort_timestamp_time")]
public string BestEffortTimestampTime { get; set; } public string BestEffortTimestampTime { get; set; } = null!;
[JsonPropertyName("pkt_duration")] [JsonPropertyName("pkt_duration")]
public int PacketDuration { get; set; } public int PacketDuration { get; set; }
[JsonPropertyName("pkt_duration_time")] [JsonPropertyName("pkt_duration_time")]
public string PacketDurationTime { get; set; } public string PacketDurationTime { get; set; } = null!;
[JsonPropertyName("pkt_pos")] [JsonPropertyName("pkt_pos")]
public long PacketPos { get; set; } public long PacketPos { get; set; }
@ -51,10 +51,10 @@ public class FFProbeFrameAnalysis
public long Height { get; set; } public long Height { get; set; }
[JsonPropertyName("pix_fmt")] [JsonPropertyName("pix_fmt")]
public string PixelFormat { get; set; } public string PixelFormat { get; set; } = null!;
[JsonPropertyName("pict_type")] [JsonPropertyName("pict_type")]
public string PictureType { get; set; } public string PictureType { get; set; } = null!;
[JsonPropertyName("coded_picture_number")] [JsonPropertyName("coded_picture_number")]
public long CodedPictureNumber { get; set; } public long CodedPictureNumber { get; set; }
@ -72,12 +72,12 @@ public class FFProbeFrameAnalysis
public int RepeatPicture { get; set; } public int RepeatPicture { get; set; }
[JsonPropertyName("chroma_location")] [JsonPropertyName("chroma_location")]
public string ChromaLocation { get; set; } public string ChromaLocation { get; set; } = null!;
} }
public class FFProbeFrames public class FFProbeFrames
{ {
[JsonPropertyName("frames")] [JsonPropertyName("frames")]
public List<FFProbeFrameAnalysis> Frames { get; set; } public List<FFProbeFrameAnalysis> Frames { get; set; } = null!;
} }
} }

View file

@ -26,7 +26,7 @@ private MediaFormat ParseFormat(Format analysisFormat)
StreamCount = analysisFormat.NbStreams, StreamCount = analysisFormat.NbStreams,
ProbeScore = analysisFormat.ProbeScore, ProbeScore = analysisFormat.ProbeScore,
BitRate = long.Parse(analysisFormat.BitRate ?? "0"), BitRate = long.Parse(analysisFormat.BitRate ?? "0"),
Tags = analysisFormat.Tags, Tags = analysisFormat.Tags.ToCaseInsensitive(),
}; };
} }
@ -70,7 +70,7 @@ private VideoStream ParseVideoStream(FFProbeStream stream)
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), Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition),
Tags = stream.Tags, Tags = stream.Tags.ToCaseInsensitive(),
}; };
} }
@ -91,7 +91,7 @@ private AudioStream ParseAudioStream(FFProbeStream stream)
Profile = stream.Profile, Profile = stream.Profile,
Language = stream.GetLanguage(), Language = stream.GetLanguage(),
Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition), Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition),
Tags = stream.Tags, Tags = stream.Tags.ToCaseInsensitive(),
}; };
} }
@ -106,15 +106,20 @@ private SubtitleStream ParseSubtitleStream(FFProbeStream stream)
Duration = MediaAnalysisUtils.ParseDuration(stream), Duration = MediaAnalysisUtils.ParseDuration(stream),
Language = stream.GetLanguage(), Language = stream.GetLanguage(),
Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition), Disposition = MediaAnalysisUtils.FormatDisposition(stream.Disposition),
Tags = stream.Tags, Tags = stream.Tags.ToCaseInsensitive(),
}; };
} }
} }
public static class MediaAnalysisUtils public static class MediaAnalysisUtils
{ {
private static readonly Regex DurationRegex = new Regex(@"^(\d+):(\d{1,2}):(\d{1,2})\.(\d{1,3})", RegexOptions.Compiled); private static readonly Regex DurationRegex = new Regex(@"^(\d+):(\d{1,2}):(\d{1,2})\.(\d{1,3})", RegexOptions.Compiled);
internal static Dictionary<string, string>? ToCaseInsensitive(this Dictionary<string, string>? dictionary)
{
return dictionary?.ToDictionary(tag => tag.Key, tag => tag.Value, StringComparer.OrdinalIgnoreCase) ?? new Dictionary<string, string>();
}
public static double DivideRatio((double, double) ratio) => ratio.Item1 / ratio.Item2; public static double DivideRatio((double, double) ratio) => ratio.Item1 / ratio.Item2;
public static (int, int) ParseRatioInt(string input, char separator) public static (int, int) ParseRatioInt(string input, char separator)
@ -185,7 +190,7 @@ public static TimeSpan ParseDuration(FFProbeStream ffProbeStream)
return null; return null;
} }
var result = new Dictionary<string, bool>(disposition.Count); var result = new Dictionary<string, bool>(disposition.Count, StringComparer.Ordinal);
foreach (var pair in disposition) foreach (var pair in disposition)
{ {

View file

@ -5,18 +5,18 @@
namespace FFMpegCore namespace FFMpegCore
{ {
public class MediaStream public abstract class MediaStream
{ {
public int Index { get; internal set; } public int Index { get; set; }
public string CodecName { get; internal set; } = null!; public string CodecName { get; set; } = null!;
public string CodecLongName { get; internal set; } = null!; public string CodecLongName { get; 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 long BitRate { get; internal set; } public long BitRate { get; set; }
public TimeSpan Duration { get; internal set; } public TimeSpan Duration { get; set; }
public string? Language { get; internal set; } public string? Language { get; set; }
public Dictionary<string, bool>? Disposition { get; internal set; } public Dictionary<string, bool>? Disposition { get; set; }
public Dictionary<string, string>? Tags { get; internal set; } public Dictionary<string, string>? Tags { get; set; }
public Codec GetCodecInfo() => FFMpeg.GetCodec(CodecName); public Codec GetCodecInfo() => FFMpeg.GetCodec(CodecName);
} }

View file

@ -6,7 +6,7 @@ namespace FFMpegCore
public class FFProbePacketAnalysis public class FFProbePacketAnalysis
{ {
[JsonPropertyName("codec_type")] [JsonPropertyName("codec_type")]
public string CodecType { get; set; } public string CodecType { get; set; } = null!;
[JsonPropertyName("stream_index")] [JsonPropertyName("stream_index")]
public int StreamIndex { get; set; } public int StreamIndex { get; set; }
@ -15,19 +15,19 @@ public class FFProbePacketAnalysis
public long Pts { get; set; } public long Pts { get; set; }
[JsonPropertyName("pts_time")] [JsonPropertyName("pts_time")]
public string PtsTime { get; set; } public string PtsTime { get; set; } = null!;
[JsonPropertyName("dts")] [JsonPropertyName("dts")]
public long Dts { get; set; } public long Dts { get; set; }
[JsonPropertyName("dts_time")] [JsonPropertyName("dts_time")]
public string DtsTime { get; set; } public string DtsTime { get; set; } = null!;
[JsonPropertyName("duration")] [JsonPropertyName("duration")]
public int Duration { get; set; } public int Duration { get; set; }
[JsonPropertyName("duration_time")] [JsonPropertyName("duration_time")]
public string DurationTime { get; set; } public string DurationTime { get; set; } = null!;
[JsonPropertyName("size")] [JsonPropertyName("size")]
public int Size { get; set; } public int Size { get; set; }
@ -36,12 +36,12 @@ public class FFProbePacketAnalysis
public long Pos { get; set; } public long Pos { get; set; }
[JsonPropertyName("flags")] [JsonPropertyName("flags")]
public string Flags { get; set; } public string Flags { get; set; } = null!;
} }
public class FFProbePackets public class FFProbePackets
{ {
[JsonPropertyName("packets")] [JsonPropertyName("packets")]
public List<FFProbePacketAnalysis> Packets { get; set; } public List<FFProbePacketAnalysis> Packets { get; set; } = null!;
} }
} }

View file

@ -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<IProcessResult> StartAndWaitForExitAsync(this ProcessArguments processArguments, CancellationToken cancellationToken = default)
{
using var instance = processArguments.Start();
return await instance.WaitForExitAsync(cancellationToken);
}
}
}

View file

@ -4,15 +4,16 @@ namespace FFMpegCore
{ {
public class VideoStream : MediaStream public class VideoStream : MediaStream
{ {
public double AvgFrameRate { get; internal set; } public double AvgFrameRate { get; set; }
public int BitsPerRawSample { get; internal set; } public int BitsPerRawSample { get; set; }
public (int Width, int Height) DisplayAspectRatio { get; internal set; } public (int Width, int Height) DisplayAspectRatio { get; set; }
public string Profile { get; internal set; } = null!; public string Profile { get; set; } = null!;
public int Width { get; internal set; } public int Width { get; set; }
public int Height { get; internal set; } public int Height { get; set; }
public double FrameRate { get; internal set; } public double FrameRate { get; set; }
public string PixelFormat { get; internal set; } = null!; public string PixelFormat { get; set; } = null!;
public int Rotation { get; set; } public int Rotation { get; set; }
public double AverageFrameRate { get; set; }
public PixelFormat GetPixelFormatInfo() => FFMpeg.GetPixelFormat(PixelFormat); public PixelFormat GetPixelFormatInfo() => FFMpeg.GetPixelFormat(PixelFormat);
} }

View file

@ -38,8 +38,8 @@ public static void RootExceptionCheck()
public static void VerifyFFMpegExists(FFOptions ffMpegOptions) public static void VerifyFFMpegExists(FFOptions ffMpegOptions)
{ {
if (_ffmpegVerified) return; if (_ffmpegVerified) return;
var (exitCode, _) = Instance.Finish(GlobalFFOptions.GetFFMpegBinaryPath(ffMpegOptions), "-version"); var result = Instance.Finish(GlobalFFOptions.GetFFMpegBinaryPath(ffMpegOptions), "-version");
_ffmpegVerified = exitCode == 0; _ffmpegVerified = result.ExitCode == 0;
if (!_ffmpegVerified) if (!_ffmpegVerified)
throw new FFMpegException(FFMpegExceptionType.Operation, "ffmpeg was not found on your system"); throw new FFMpegException(FFMpegExceptionType.Operation, "ffmpeg was not found on your system");
} }

View file

@ -27,8 +27,8 @@ public static void RootExceptionCheck()
public static void VerifyFFProbeExists(FFOptions ffMpegOptions) public static void VerifyFFProbeExists(FFOptions ffMpegOptions)
{ {
if (_ffprobeVerified) return; if (_ffprobeVerified) return;
var (exitCode, _) = Instance.Finish(GlobalFFOptions.GetFFProbeBinaryPath(ffMpegOptions), "-version"); var result = Instance.Finish(GlobalFFOptions.GetFFProbeBinaryPath(ffMpegOptions), "-version");
_ffprobeVerified = exitCode == 0; _ffprobeVerified = result.ExitCode == 0;
if (!_ffprobeVerified) if (!_ffprobeVerified)
throw new FFProbeException("ffprobe was not found on your system"); throw new FFProbeException("ffprobe was not found on your system");
} }

View file

@ -3,14 +3,14 @@
[![GitHub issues](https://img.shields.io/github/issues/rosenbjerg/FFMpegCore)](https://github.com/rosenbjerg/FFMpegCore/issues) [![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 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) [![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 A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications. Supports both synchronous and asynchronous calls
# API # API
## FFProbe ## FFProbe
Use FFProbe to analyze media files: Use FFProbe to analyze media files:
```csharp ```csharp
@ -21,12 +21,12 @@ or
var mediaInfo = FFProbe.Analyse(inputPath); var mediaInfo = FFProbe.Analyse(inputPath);
``` ```
## FFMpeg ## FFMpeg
Use FFMpeg to convert your media files. Use FFMpeg to convert your media files.
Easily build your FFMpeg arguments using the fluent argument builder: Easily build your FFMpeg arguments using the fluent argument builder:
Convert input file to h264/aac scaled to 720p w/ faststart, for web playback Convert input file to h264/aac scaled to 720p w/ faststart, for web playback
```csharp ```csharp
FFMpegArguments FFMpegArguments
.FromFileInput(inputPath) .FromFileInput(inputPath)
@ -192,7 +192,7 @@ await FFMpegArguments
.Configure(options => options.WorkingDirectory = "./CurrentRunWorkingDir") .Configure(options => options.WorkingDirectory = "./CurrentRunWorkingDir")
.Configure(options => options.TemporaryFilesFolder = "./CurrentRunTmpFolder") .Configure(options => options.TemporaryFilesFolder = "./CurrentRunTmpFolder")
.ProcessAsynchronously(); .ProcessAsynchronously();
``` ```
### Option 2 ### Option 2