mirror of
https://github.com/rosenbjerg/FFMpegCore.git
synced 2024-11-10 08:34:12 +01:00
commit
cbfe550434
35 changed files with 584 additions and 266 deletions
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
|
@ -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
|
||||||
|
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
@ -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
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
|
@ -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.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -113,13 +113,11 @@ public void Video_ToMP4_Args_Pipe_DifferentImageSizes()
|
||||||
};
|
};
|
||||||
|
|
||||||
var videoFramesSource = new RawVideoPipeSource(frames);
|
var videoFramesSource = new RawVideoPipeSource(frames);
|
||||||
var ex = Assert.ThrowsException<FFMpegException>(() => FFMpegArguments
|
var ex = Assert.ThrowsException<FFMpegStreamFormatException>(() => FFMpegArguments
|
||||||
.FromPipeInput(videoFramesSource)
|
.FromPipeInput(videoFramesSource)
|
||||||
.OutputToFile(outputFile, false, opt => opt
|
.OutputToFile(outputFile, false, opt => opt
|
||||||
.WithVideoCodec(VideoCodec.LibX264))
|
.WithVideoCodec(VideoCodec.LibX264))
|
||||||
.ProcessSynchronously());
|
.ProcessSynchronously());
|
||||||
|
|
||||||
Assert.IsInstanceOfType(ex.GetBaseException(), typeof(FFMpegStreamFormatException));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -135,13 +133,11 @@ public async Task Video_ToMP4_Args_Pipe_DifferentImageSizes_Async()
|
||||||
};
|
};
|
||||||
|
|
||||||
var videoFramesSource = new RawVideoPipeSource(frames);
|
var videoFramesSource = new RawVideoPipeSource(frames);
|
||||||
var ex = await Assert.ThrowsExceptionAsync<FFMpegException>(() => FFMpegArguments
|
var ex = await Assert.ThrowsExceptionAsync<FFMpegStreamFormatException>(() => FFMpegArguments
|
||||||
.FromPipeInput(videoFramesSource)
|
.FromPipeInput(videoFramesSource)
|
||||||
.OutputToFile(outputFile, false, opt => opt
|
.OutputToFile(outputFile, false, opt => opt
|
||||||
.WithVideoCodec(VideoCodec.LibX264))
|
.WithVideoCodec(VideoCodec.LibX264))
|
||||||
.ProcessAsynchronously());
|
.ProcessAsynchronously());
|
||||||
|
|
||||||
Assert.IsInstanceOfType(ex.GetBaseException(), typeof(FFMpegStreamFormatException));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod, Timeout(10000)]
|
[TestMethod, Timeout(10000)]
|
||||||
|
@ -156,13 +152,11 @@ public void Video_ToMP4_Args_Pipe_DifferentPixelFormats()
|
||||||
};
|
};
|
||||||
|
|
||||||
var videoFramesSource = new RawVideoPipeSource(frames);
|
var videoFramesSource = new RawVideoPipeSource(frames);
|
||||||
var ex = Assert.ThrowsException<FFMpegException>(() => FFMpegArguments
|
var ex = Assert.ThrowsException<FFMpegStreamFormatException>(() => FFMpegArguments
|
||||||
.FromPipeInput(videoFramesSource)
|
.FromPipeInput(videoFramesSource)
|
||||||
.OutputToFile(outputFile, false, opt => opt
|
.OutputToFile(outputFile, false, opt => opt
|
||||||
.WithVideoCodec(VideoCodec.LibX264))
|
.WithVideoCodec(VideoCodec.LibX264))
|
||||||
.ProcessSynchronously());
|
.ProcessSynchronously());
|
||||||
|
|
||||||
Assert.IsInstanceOfType(ex.GetBaseException(), typeof(FFMpegStreamFormatException));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -178,13 +172,11 @@ public async Task Video_ToMP4_Args_Pipe_DifferentPixelFormats_Async()
|
||||||
};
|
};
|
||||||
|
|
||||||
var videoFramesSource = new RawVideoPipeSource(frames);
|
var videoFramesSource = new RawVideoPipeSource(frames);
|
||||||
var ex = await Assert.ThrowsExceptionAsync<FFMpegException>(() => FFMpegArguments
|
var ex = await Assert.ThrowsExceptionAsync<FFMpegStreamFormatException>(() => FFMpegArguments
|
||||||
.FromPipeInput(videoFramesSource)
|
.FromPipeInput(videoFramesSource)
|
||||||
.OutputToFile(outputFile, false, opt => opt
|
.OutputToFile(outputFile, false, opt => opt
|
||||||
.WithVideoCodec(VideoCodec.LibX264))
|
.WithVideoCodec(VideoCodec.LibX264))
|
||||||
.ProcessAsynchronously());
|
.ProcessAsynchronously());
|
||||||
|
|
||||||
Assert.IsInstanceOfType(ex.GetBaseException(), typeof(FFMpegStreamFormatException));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestMethod, Timeout(10000)]
|
[TestMethod, Timeout(10000)]
|
||||||
|
@ -468,7 +460,7 @@ public void Video_Join_Image_Sequence()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var outputFile = new TemporaryFile("out.mp4");
|
using var outputFile = new TemporaryFile("out.mp4");
|
||||||
var success = FFMpeg.JoinImageSequence(outputFile, images: imageSet.ToArray());
|
var success = FFMpeg.JoinImageSequence(outputFile, images: imageSet.ToArray());
|
||||||
Assert.IsTrue(success);
|
Assert.IsTrue(success);
|
||||||
var result = FFProbe.Analyse(outputFile);
|
var result = FFProbe.Analyse(outputFile);
|
||||||
|
@ -544,7 +536,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);
|
||||||
|
@ -596,6 +588,27 @@ public async Task Video_Cancel_Async()
|
||||||
Assert.IsFalse(result);
|
Assert.IsFalse(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod, Timeout(10000)]
|
||||||
|
public void Video_Cancel()
|
||||||
|
{
|
||||||
|
var outputFile = new TemporaryFile("out.mp4");
|
||||||
|
var task = FFMpegArguments
|
||||||
|
.FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args
|
||||||
|
.WithCustomArgument("-re")
|
||||||
|
.ForceFormat("lavfi"))
|
||||||
|
.OutputToFile(outputFile, false, opt => opt
|
||||||
|
.WithAudioCodec(AudioCodec.Aac)
|
||||||
|
.WithVideoCodec(VideoCodec.LibX264)
|
||||||
|
.WithSpeedPreset(Speed.VeryFast))
|
||||||
|
.CancellableThrough(out var cancel);
|
||||||
|
|
||||||
|
Task.Delay(300).ContinueWith((_) => cancel());
|
||||||
|
|
||||||
|
var result = task.ProcessSynchronously(false);
|
||||||
|
|
||||||
|
Assert.IsFalse(result);
|
||||||
|
}
|
||||||
|
|
||||||
[TestMethod, Timeout(10000)]
|
[TestMethod, Timeout(10000)]
|
||||||
public async Task Video_Cancel_Async_With_Timeout()
|
public async Task Video_Cancel_Async_With_Timeout()
|
||||||
{
|
{
|
||||||
|
@ -615,11 +628,10 @@ public async Task Video_Cancel_Async_With_Timeout()
|
||||||
await Task.Delay(300);
|
await Task.Delay(300);
|
||||||
cancel();
|
cancel();
|
||||||
|
|
||||||
var result = await task;
|
await task;
|
||||||
|
|
||||||
var outputInfo = await FFProbe.AnalyseAsync(outputFile);
|
var outputInfo = await FFProbe.AnalyseAsync(outputFile);
|
||||||
|
|
||||||
Assert.IsTrue(result);
|
|
||||||
Assert.IsNotNull(outputInfo);
|
Assert.IsNotNull(outputInfo);
|
||||||
Assert.AreEqual(320, outputInfo.PrimaryVideoStream!.Width);
|
Assert.AreEqual(320, outputInfo.PrimaryVideoStream!.Width);
|
||||||
Assert.AreEqual(240, outputInfo.PrimaryVideoStream.Height);
|
Assert.AreEqual(240, outputInfo.PrimaryVideoStream.Height);
|
||||||
|
@ -645,14 +657,58 @@ public async Task Video_Cancel_CancellationToken_Async()
|
||||||
.CancellableThrough(cts.Token)
|
.CancellableThrough(cts.Token)
|
||||||
.ProcessAsynchronously(false);
|
.ProcessAsynchronously(false);
|
||||||
|
|
||||||
await Task.Delay(300);
|
cts.CancelAfter(300);
|
||||||
cts.Cancel();
|
|
||||||
|
|
||||||
var result = await task;
|
var result = await task;
|
||||||
|
|
||||||
Assert.IsFalse(result);
|
Assert.IsFalse(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod, Timeout(10000)]
|
||||||
|
public async Task Video_Cancel_CancellationToken_Async_Throws()
|
||||||
|
{
|
||||||
|
var outputFile = new TemporaryFile("out.mp4");
|
||||||
|
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
var task = FFMpegArguments
|
||||||
|
.FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args
|
||||||
|
.WithCustomArgument("-re")
|
||||||
|
.ForceFormat("lavfi"))
|
||||||
|
.OutputToFile(outputFile, false, opt => opt
|
||||||
|
.WithAudioCodec(AudioCodec.Aac)
|
||||||
|
.WithVideoCodec(VideoCodec.LibX264)
|
||||||
|
.WithSpeedPreset(Speed.VeryFast))
|
||||||
|
.CancellableThrough(cts.Token)
|
||||||
|
.ProcessAsynchronously();
|
||||||
|
|
||||||
|
cts.CancelAfter(300);
|
||||||
|
|
||||||
|
await Assert.ThrowsExceptionAsync<OperationCanceledException>(() => task);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod, Timeout(10000)]
|
||||||
|
public void Video_Cancel_CancellationToken_Throws()
|
||||||
|
{
|
||||||
|
var outputFile = new TemporaryFile("out.mp4");
|
||||||
|
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
var task = FFMpegArguments
|
||||||
|
.FromFileInput("testsrc2=size=320x240[out0]; sine[out1]", false, args => args
|
||||||
|
.WithCustomArgument("-re")
|
||||||
|
.ForceFormat("lavfi"))
|
||||||
|
.OutputToFile(outputFile, false, opt => opt
|
||||||
|
.WithAudioCodec(AudioCodec.Aac)
|
||||||
|
.WithVideoCodec(VideoCodec.LibX264)
|
||||||
|
.WithSpeedPreset(Speed.VeryFast))
|
||||||
|
.CancellableThrough(cts.Token);
|
||||||
|
|
||||||
|
cts.CancelAfter(300);
|
||||||
|
|
||||||
|
Assert.ThrowsException<OperationCanceledException>(() => task.ProcessSynchronously());
|
||||||
|
}
|
||||||
|
|
||||||
[TestMethod, Timeout(10000)]
|
[TestMethod, Timeout(10000)]
|
||||||
public async Task Video_Cancel_CancellationToken_Async_With_Timeout()
|
public async Task Video_Cancel_CancellationToken_Async_With_Timeout()
|
||||||
{
|
{
|
||||||
|
@ -671,14 +727,12 @@ public async Task Video_Cancel_CancellationToken_Async_With_Timeout()
|
||||||
.CancellableThrough(cts.Token, 8000)
|
.CancellableThrough(cts.Token, 8000)
|
||||||
.ProcessAsynchronously(false);
|
.ProcessAsynchronously(false);
|
||||||
|
|
||||||
await Task.Delay(300);
|
cts.CancelAfter(300);
|
||||||
cts.Cancel();
|
|
||||||
|
|
||||||
var result = await task;
|
await task;
|
||||||
|
|
||||||
var outputInfo = await FFProbe.AnalyseAsync(outputFile);
|
var outputInfo = await FFProbe.AnalyseAsync(outputFile);
|
||||||
|
|
||||||
Assert.IsTrue(result);
|
|
||||||
Assert.IsNotNull(outputInfo);
|
Assert.IsNotNull(outputInfo);
|
||||||
Assert.AreEqual(320, outputInfo.PrimaryVideoStream!.Width);
|
Assert.AreEqual(320, outputInfo.PrimaryVideoStream!.Width);
|
||||||
Assert.AreEqual(240, outputInfo.PrimaryVideoStream.Height);
|
Assert.AreEqual(240, outputInfo.PrimaryVideoStream.Height);
|
||||||
|
|
|
@ -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
|
||||||
|
|
28
FFMpegCore/FFMpeg/Arguments/AudibleEncryptionKeyArgument.cs
Normal file
28
FFMpegCore/FFMpeg/Arguments/AudibleEncryptionKeyArgument.cs
Normal 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}";
|
||||||
|
}
|
||||||
|
}
|
14
FFMpegCore/FFMpeg/Arguments/ID3V2VersionArgument.cs
Normal file
14
FFMpegCore/FFMpeg/Arguments/ID3V2VersionArgument.cs
Normal 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}";
|
||||||
|
}
|
||||||
|
}
|
16
FFMpegCore/FFMpeg/Arguments/IDynamicArgument.cs
Normal file
16
FFMpegCore/FFMpeg/Arguments/IDynamicArgument.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
using System.IO.Pipes;
|
using System;
|
||||||
|
using System.IO.Pipes;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using FFMpegCore.Pipes;
|
using FFMpegCore.Pipes;
|
||||||
|
@ -23,7 +24,7 @@ protected override async Task ProcessDataAsync(CancellationToken token)
|
||||||
{
|
{
|
||||||
await Pipe.WaitForConnectionAsync(token).ConfigureAwait(false);
|
await Pipe.WaitForConnectionAsync(token).ConfigureAwait(false);
|
||||||
if (!Pipe.IsConnected)
|
if (!Pipe.IsConnected)
|
||||||
throw new TaskCanceledException();
|
throw new OperationCanceledException();
|
||||||
await Writer.WriteAsync(Pipe, token).ConfigureAwait(false);
|
await Writer.WriteAsync(Pipe, token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
64
FFMpegCore/FFMpeg/Arguments/MapMetadataArgument.cs
Normal file
64
FFMpegCore/FFMpeg/Arguments/MapMetadataArgument.cs
Normal 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()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,14 +42,15 @@ public async Task During(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await ProcessDataAsync(cancellationToken).ConfigureAwait(false);
|
await ProcessDataAsync(cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (TaskCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
Debug.WriteLine($"ProcessDataAsync on {GetType().Name} cancelled");
|
Debug.WriteLine($"ProcessDataAsync on {GetType().Name} cancelled");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
Debug.WriteLine($"Disconnecting NamedPipeServerStream on {GetType().Name}");
|
Debug.WriteLine($"Disconnecting NamedPipeServerStream on {GetType().Name}");
|
||||||
Pipe?.Disconnect();
|
if (Pipe is { IsConnected: true })
|
||||||
|
Pipe.Disconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,13 +247,18 @@ 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
|
||||||
.Loop(1))
|
.Loop(1)
|
||||||
|
.ForceFormat("image2"))
|
||||||
.AddFileInput(audio)
|
.AddFileInput(audio)
|
||||||
.OutputToFile(output, true, options => options
|
.OutputToFile(output, true, options => options
|
||||||
|
.ForcePixelFormat("yuv420p")
|
||||||
.WithVideoCodec(VideoCodec.LibX264)
|
.WithVideoCodec(VideoCodec.LibX264)
|
||||||
.WithConstantRateFactor(21)
|
.WithConstantRateFactor(21)
|
||||||
.WithAudioBitrate(AudioQuality.Normal)
|
.WithAudioBitrate(AudioQuality.Normal)
|
||||||
|
@ -319,6 +325,7 @@ public static bool JoinImageSequence(string output, double frameRate = 30, param
|
||||||
return FFMpegArguments
|
return FFMpegArguments
|
||||||
.FromFileInput(Path.Combine(tempFolderName, "%09d.png"), false)
|
.FromFileInput(Path.Combine(tempFolderName, "%09d.png"), false)
|
||||||
.OutputToFile(output, true, options => options
|
.OutputToFile(output, true, options => options
|
||||||
|
.ForcePixelFormat("yuv420p")
|
||||||
.Resize(firstImage.Width, firstImage.Height)
|
.Resize(firstImage.Width, firstImage.Height)
|
||||||
.WithFramerate(frameRate))
|
.WithFramerate(frameRate))
|
||||||
.ProcessSynchronously();
|
.ProcessSynchronously();
|
||||||
|
@ -417,15 +424,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 +470,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 +481,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 +554,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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,85 +86,83 @@ 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);
|
||||||
|
|
||||||
void OnCancelEvent(object sender, int timeout)
|
|
||||||
{
|
|
||||||
instance.SendInput("q");
|
|
||||||
|
|
||||||
if (!cancellationTokenSource.Token.WaitHandle.WaitOne(timeout, true))
|
IProcessResult? processResult = null;
|
||||||
{
|
|
||||||
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 (OperationCanceledException)
|
||||||
{
|
{
|
||||||
if (!HandleException(throwOnError, e, instance.ErrorData)) return false;
|
if (throwOnError)
|
||||||
}
|
throw;
|
||||||
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 (OperationCanceledException)
|
||||||
|
{
|
||||||
|
if (throwOnError)
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
var cancelled = false;
|
||||||
void OnCancelEvent(object sender, int timeout)
|
void OnCancelEvent(object sender, int timeout)
|
||||||
{
|
{
|
||||||
|
cancelled = true;
|
||||||
instance.SendInput("q");
|
instance.SendInput("q");
|
||||||
|
|
||||||
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)
|
processResult = t.Result;
|
||||||
{
|
cancellationTokenSource.Cancel();
|
||||||
if (!HandleException(throwOnError, e, instance.ErrorData)) return false;
|
_ffMpegArguments.Post();
|
||||||
|
}), _ffMpegArguments.During(cancellationTokenSource.Token)).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (cancelled)
|
||||||
|
{
|
||||||
|
throw new OperationCanceledException("ffmpeg processing was cancelled");
|
||||||
|
}
|
||||||
|
|
||||||
|
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 +188,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,30 +201,29 @@ 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)
|
||||||
private static bool HandleException(bool throwOnError, Exception e, IReadOnlyList<string> errorData)
|
|
||||||
{
|
{
|
||||||
if (!throwOnError)
|
_onError?.Invoke(msg);
|
||||||
return false;
|
|
||||||
|
|
||||||
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);
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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!;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
@ -163,9 +152,10 @@ private static IMediaAnalysis ParseOutput(Instance instance)
|
||||||
if (ffprobeAnalysis?.Format == null)
|
if (ffprobeAnalysis?.Format == null)
|
||||||
throw new FormatNullException();
|
throw new FormatNullException();
|
||||||
|
|
||||||
|
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
|
||||||
|
@ -174,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
|
||||||
|
@ -186,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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,9 @@ public class FFProbeAnalysis
|
||||||
|
|
||||||
[JsonPropertyName("format")]
|
[JsonPropertyName("format")]
|
||||||
public Format Format { get; set; } = null!;
|
public Format Format { get; set; } = null!;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public IReadOnlyList<string> ErrorData { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class FFProbeStream : ITagsContainer, IDispositionContainer
|
public class FFProbeStream : ITagsContainer, IDispositionContainer
|
||||||
|
|
|
@ -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!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,5 +13,6 @@ public interface IMediaAnalysis
|
||||||
List<VideoStream> VideoStreams { get; }
|
List<VideoStream> VideoStreams { get; }
|
||||||
List<AudioStream> AudioStreams { get; }
|
List<AudioStream> AudioStreams { get; }
|
||||||
List<SubtitleStream> SubtitleStreams { get; }
|
List<SubtitleStream> SubtitleStreams { get; }
|
||||||
|
IReadOnlyList<string> ErrorData { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ internal MediaAnalysis(FFProbeAnalysis analysis)
|
||||||
VideoStreams = analysis.Streams.Where(stream => stream.CodecType == "video").Select(ParseVideoStream).ToList();
|
VideoStreams = analysis.Streams.Where(stream => stream.CodecType == "video").Select(ParseVideoStream).ToList();
|
||||||
AudioStreams = analysis.Streams.Where(stream => stream.CodecType == "audio").Select(ParseAudioStream).ToList();
|
AudioStreams = analysis.Streams.Where(stream => stream.CodecType == "audio").Select(ParseAudioStream).ToList();
|
||||||
SubtitleStreams = analysis.Streams.Where(stream => stream.CodecType == "subtitle").Select(ParseSubtitleStream).ToList();
|
SubtitleStreams = analysis.Streams.Where(stream => stream.CodecType == "subtitle").Select(ParseSubtitleStream).ToList();
|
||||||
|
ErrorData = analysis.ErrorData ?? new List<string>().AsReadOnly();
|
||||||
}
|
}
|
||||||
|
|
||||||
private MediaFormat ParseFormat(Format analysisFormat)
|
private MediaFormat ParseFormat(Format analysisFormat)
|
||||||
|
@ -25,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(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,6 +46,7 @@ private MediaFormat ParseFormat(Format analysisFormat)
|
||||||
public List<VideoStream> VideoStreams { get; }
|
public List<VideoStream> VideoStreams { get; }
|
||||||
public List<AudioStream> AudioStreams { get; }
|
public List<AudioStream> AudioStreams { get; }
|
||||||
public List<SubtitleStream> SubtitleStreams { get; }
|
public List<SubtitleStream> SubtitleStreams { get; }
|
||||||
|
public IReadOnlyList<string> ErrorData { get; }
|
||||||
|
|
||||||
private VideoStream ParseVideoStream(FFProbeStream stream)
|
private VideoStream ParseVideoStream(FFProbeStream stream)
|
||||||
{
|
{
|
||||||
|
@ -68,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(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,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(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,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)
|
||||||
|
@ -183,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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
20
FFMpegCore/FFProbe/ProcessArgumentsExtensions.cs
Normal file
20
FFMpegCore/FFProbe/ProcessArgumentsExtensions.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue