mirror of
https://github.com/rosenbjerg/FFMpegCore.git
synced 2024-11-10 00:24:14 +01:00
Merge branch 'master' into add-audiofilters
Former-commit-id: b53d8ebadc
This commit is contained in:
commit
30c9ea85c4
33 changed files with 472 additions and 385 deletions
24
.github/workflows/ci.yml
vendored
24
.github/workflows/ci.yml
vendored
|
@ -1,13 +1,6 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- .github/workflows/ci.yml
|
||||
- FFMpegCore/**
|
||||
- FFMpegCore.Test/**
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
@ -25,13 +18,20 @@ jobs:
|
|||
os: [windows-latest, ubuntu-latest]
|
||||
timeout-minutes: 6
|
||||
steps:
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare .NET
|
||||
uses: actions/setup-dotnet@v1
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
dotnet-version: '7.0.x'
|
||||
|
||||
- name: Prepare FFMpeg
|
||||
uses: FedericoCarboni/setup-ffmpeg@v1
|
||||
uses: Iamshankhadeep/setup-ffmpeg@v1.2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
version: "4.4"
|
||||
|
||||
- name: Test with dotnet
|
||||
run: dotnet test --logger GitHubActions
|
||||
run: dotnet test FFMpegCore.sln --logger GitHubActions
|
||||
|
|
21
.github/workflows/release.yml
vendored
21
.github/workflows/release.yml
vendored
|
@ -8,13 +8,16 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Prepare .NET
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: '6.0.x'
|
||||
- name: Build solution
|
||||
run: dotnet build --output build -c Release
|
||||
- name: Publish NuGet package
|
||||
run: dotnet nuget push "build/*.nupkg" --source nuget.org --api-key ${{ secrets.NUGET_TOKEN }}
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare .NET
|
||||
uses: actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: '7.0.x'
|
||||
|
||||
- name: Build solution
|
||||
run: dotnet pack FFMpegCore.sln --output build -c Release
|
||||
|
||||
- name: Publish NuGet package
|
||||
run: dotnet nuget push build/*.nupkg --source nuget.org --api-key ${{ secrets.NUGET_TOKEN }}
|
||||
|
||||
|
|
15
Directory.Build.props
Normal file
15
Directory.Build.props
Normal file
|
@ -0,0 +1,15 @@
|
|||
<Project>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
<AssemblyVersion>5.0.0.0</AssemblyVersion>
|
||||
<LangVersion>default</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<RepositoryType>GitHub</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/rosenbjerg/FFMpegCore</RepositoryUrl>
|
||||
<PackageProjectUrl>https://github.com/rosenbjerg/FFMpegCore</PackageProjectUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
</PropertyGroup>
|
||||
</Project>
|
|
@ -6,6 +6,7 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FFMpegCore.Extensions.System.Drawing.Common\FFMpegCore.Extensions.System.Drawing.Common.csproj" />
|
||||
<ProjectReference Include="..\FFMpegCore\FFMpegCore.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
using FFMpegCore;
|
||||
using FFMpegCore.Enums;
|
||||
using FFMpegCore.Pipes;
|
||||
using FFMpegCore.Extend;
|
||||
using FFMpegCore.Extensions.System.Drawing.Common;
|
||||
|
||||
var inputPath = "/path/to/input";
|
||||
var outputPath = "/path/to/output";
|
||||
|
@ -34,7 +34,7 @@
|
|||
|
||||
{
|
||||
// process the snapshot in-memory and use the Bitmap directly
|
||||
var bitmap = FFMpeg.Snapshot(inputPath, new Size(200, 400), TimeSpan.FromMinutes(1));
|
||||
var bitmap = FFMpegImage.Snapshot(inputPath, new Size(200, 400), TimeSpan.FromMinutes(1));
|
||||
|
||||
// or persists the image on the drive
|
||||
FFMpeg.Snapshot(inputPath, outputPath, new Size(200, 400), TimeSpan.FromMinutes(1));
|
||||
|
@ -61,7 +61,7 @@ await FFMpegArguments
|
|||
}
|
||||
|
||||
{
|
||||
FFMpeg.JoinImageSequence(@"..\joined_video.mp4", frameRate: 1,
|
||||
FFMpegImage.JoinImageSequence(@"..\joined_video.mp4", frameRate: 1,
|
||||
ImageInfo.FromPath(@"..\1.png"),
|
||||
ImageInfo.FromPath(@"..\2.png"),
|
||||
ImageInfo.FromPath(@"..\3.png")
|
||||
|
@ -83,9 +83,9 @@ await FFMpegArguments
|
|||
|
||||
var inputImagePath = "/path/to/input/image";
|
||||
{
|
||||
FFMpeg.PosterWithAudio(inputPath, inputAudioPath, outputPath);
|
||||
FFMpegImage.PosterWithAudio(inputPath, inputAudioPath, outputPath);
|
||||
// or
|
||||
var image = Image.FromFile(inputImagePath);
|
||||
using var image = Image.FromFile(inputImagePath);
|
||||
image.AddAudio(inputAudioPath, outputPath);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
using System.Drawing;
|
||||
using System.IO;
|
||||
|
||||
namespace FFMpegCore.Extend
|
||||
namespace FFMpegCore.Extensions.System.Drawing.Common
|
||||
{
|
||||
public static class BitmapExtensions
|
||||
{
|
||||
|
@ -12,7 +12,7 @@ public static bool AddAudio(this Image poster, string audio, string output)
|
|||
poster.Save(destination);
|
||||
try
|
||||
{
|
||||
return FFMpeg.PosterWithAudio(destination, audio, output);
|
||||
return FFMpegImage.PosterWithAudio(destination, audio, output);
|
||||
}
|
||||
finally
|
||||
{
|
|
@ -7,7 +7,7 @@
|
|||
using System.Threading.Tasks;
|
||||
using FFMpegCore.Pipes;
|
||||
|
||||
namespace FFMpegCore.Extend
|
||||
namespace FFMpegCore.Extensions.System.Drawing.Common
|
||||
{
|
||||
public class BitmapVideoFrameWrapper : IVideoFrame, IDisposable
|
||||
{
|
|
@ -0,0 +1,21 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<IsPackable>true</IsPackable>
|
||||
<Description>Image extension for FFMpegCore using System.Common.Drawing</Description>
|
||||
<PackageVersion>5.0.0</PackageVersion>
|
||||
<PackageReleaseNotes>
|
||||
</PackageReleaseNotes>
|
||||
<PackageTags>ffmpeg ffprobe convert video audio mediafile resize analyze muxing</PackageTags>
|
||||
<Authors>Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev</Authors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FFMpegCore\FFMpegCore.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
138
FFMpegCore.Extensions.System.Drawing.Common/FFMpegImage.cs
Normal file
138
FFMpegCore.Extensions.System.Drawing.Common/FFMpegImage.cs
Normal file
|
@ -0,0 +1,138 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using FFMpegCore.Enums;
|
||||
using FFMpegCore.Helpers;
|
||||
using FFMpegCore.Pipes;
|
||||
|
||||
namespace FFMpegCore.Extensions.System.Drawing.Common
|
||||
{
|
||||
public static class FFMpegImage
|
||||
{
|
||||
public static void ConversionSizeExceptionCheck(Image image)
|
||||
=> FFMpegHelper.ConversionSizeExceptionCheck(image.Size.Width, image.Size.Height);
|
||||
|
||||
/// <summary>
|
||||
/// Converts an image sequence to a video.
|
||||
/// </summary>
|
||||
/// <param name="output">Output video file.</param>
|
||||
/// <param name="frameRate">FPS</param>
|
||||
/// <param name="images">Image sequence collection</param>
|
||||
/// <returns>Output video information.</returns>
|
||||
public static bool JoinImageSequence(string output, double frameRate = 30, params ImageInfo[] images)
|
||||
{
|
||||
var tempFolderName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, Guid.NewGuid().ToString());
|
||||
var temporaryImageFiles = images.Select((imageInfo, index) =>
|
||||
{
|
||||
using var image = Image.FromFile(imageInfo.FullName);
|
||||
FFMpegHelper.ConversionSizeExceptionCheck(image.Width, image.Height);
|
||||
var destinationPath = Path.Combine(tempFolderName, $"{index.ToString().PadLeft(9, '0')}{imageInfo.Extension}");
|
||||
Directory.CreateDirectory(tempFolderName);
|
||||
File.Copy(imageInfo.FullName, destinationPath);
|
||||
return destinationPath;
|
||||
}).ToArray();
|
||||
|
||||
var firstImage = images.First();
|
||||
try
|
||||
{
|
||||
return FFMpegArguments
|
||||
.FromFileInput(Path.Combine(tempFolderName, "%09d.png"), false)
|
||||
.OutputToFile(output, true, options => options
|
||||
.ForcePixelFormat("yuv420p")
|
||||
.Resize(firstImage.Width, firstImage.Height)
|
||||
.WithFramerate(frameRate))
|
||||
.ProcessSynchronously();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Cleanup(temporaryImageFiles);
|
||||
Directory.Delete(tempFolderName);
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Adds a poster image to an audio file.
|
||||
/// </summary>
|
||||
/// <param name="image">Source image file.</param>
|
||||
/// <param name="audio">Source audio file.</param>
|
||||
/// <param name="output">Output video file.</param>
|
||||
/// <returns></returns>
|
||||
public static bool PosterWithAudio(string image, string audio, string output)
|
||||
{
|
||||
FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.Mp4);
|
||||
using (var img = Image.FromFile(image))
|
||||
FFMpegHelper.ConversionSizeExceptionCheck(img.Width, img.Height);
|
||||
|
||||
return FFMpegArguments
|
||||
.FromFileInput(image, false, options => options
|
||||
.Loop(1)
|
||||
.ForceFormat("image2"))
|
||||
.AddFileInput(audio)
|
||||
.OutputToFile(output, true, options => options
|
||||
.ForcePixelFormat("yuv420p")
|
||||
.WithVideoCodec(VideoCodec.LibX264)
|
||||
.WithConstantRateFactor(21)
|
||||
.WithAudioBitrate(AudioQuality.Normal)
|
||||
.UsingShortest())
|
||||
.ProcessSynchronously();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves a 'png' thumbnail to an in-memory bitmap
|
||||
/// </summary>
|
||||
/// <param name="input">Source video file.</param>
|
||||
/// <param name="captureTime">Seek position where the thumbnail should be taken.</param>
|
||||
/// <param name="size">Thumbnail size. If width or height equal 0, the other will be computed automatically.</param>
|
||||
/// <param name="streamIndex">Selected video stream index.</param>
|
||||
/// <param name="inputFileIndex">Input file index</param>
|
||||
/// <returns>Bitmap with the requested snapshot.</returns>
|
||||
public static Bitmap Snapshot(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0)
|
||||
{
|
||||
var source = FFProbe.Analyse(input);
|
||||
var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex);
|
||||
using var ms = new MemoryStream();
|
||||
|
||||
arguments
|
||||
.OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options
|
||||
.ForceFormat("rawvideo")))
|
||||
.ProcessSynchronously();
|
||||
|
||||
ms.Position = 0;
|
||||
using var bitmap = new Bitmap(ms);
|
||||
return bitmap.Clone(new Rectangle(0, 0, bitmap.Width, bitmap.Height), bitmap.PixelFormat);
|
||||
}
|
||||
/// <summary>
|
||||
/// Saves a 'png' thumbnail to an in-memory bitmap
|
||||
/// </summary>
|
||||
/// <param name="input">Source video file.</param>
|
||||
/// <param name="captureTime">Seek position where the thumbnail should be taken.</param>
|
||||
/// <param name="size">Thumbnail size. If width or height equal 0, the other will be computed automatically.</param>
|
||||
/// <param name="streamIndex">Selected video stream index.</param>
|
||||
/// <param name="inputFileIndex">Input file index</param>
|
||||
/// <returns>Bitmap with the requested snapshot.</returns>
|
||||
public static async Task<Bitmap> SnapshotAsync(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0)
|
||||
{
|
||||
var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false);
|
||||
var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex);
|
||||
using var ms = new MemoryStream();
|
||||
|
||||
await arguments
|
||||
.OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options
|
||||
.ForceFormat("rawvideo")))
|
||||
.ProcessAsynchronously();
|
||||
|
||||
ms.Position = 0;
|
||||
return new Bitmap(ms);
|
||||
}
|
||||
private static void Cleanup(IEnumerable<string> pathList)
|
||||
{
|
||||
foreach (var path in pathList)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,6 +9,8 @@
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using FFMpegCore.Extensions.System.Drawing.Common;
|
||||
using FFMpegCore.Test.Utilities;
|
||||
|
||||
namespace FFMpegCore.Test
|
||||
{
|
||||
|
@ -64,11 +66,11 @@ public void Audio_Add()
|
|||
Assert.IsTrue(File.Exists(outputFile));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[WindowsOnlyTestMethod]
|
||||
public void Image_AddAudio()
|
||||
{
|
||||
using var outputFile = new TemporaryFile("out.mp4");
|
||||
FFMpeg.PosterWithAudio(TestResources.PngImage, TestResources.Mp3Audio, outputFile);
|
||||
FFMpegImage.PosterWithAudio(TestResources.PngImage, TestResources.Mp3Audio, outputFile);
|
||||
var analysis = FFProbe.Analyse(TestResources.Mp3Audio);
|
||||
Assert.IsTrue(analysis.Duration.TotalSeconds > 0);
|
||||
Assert.IsTrue(File.Exists(outputFile));
|
||||
|
@ -239,7 +241,7 @@ public void Audio_Pan_ToMono()
|
|||
|
||||
Assert.IsTrue(success);
|
||||
Assert.AreEqual(1, mediaAnalysis.AudioStreams.Count);
|
||||
Assert.AreEqual("mono", mediaAnalysis.PrimaryAudioStream.ChannelLayout);
|
||||
Assert.AreEqual("mono", mediaAnalysis.PrimaryAudioStream!.ChannelLayout);
|
||||
}
|
||||
|
||||
[TestMethod, Timeout(10000)]
|
||||
|
@ -257,7 +259,7 @@ public void Audio_Pan_ToMonoNoDefinitions()
|
|||
|
||||
Assert.IsTrue(success);
|
||||
Assert.AreEqual(1, mediaAnalysis.AudioStreams.Count);
|
||||
Assert.AreEqual("mono", mediaAnalysis.PrimaryAudioStream.ChannelLayout);
|
||||
Assert.AreEqual("mono", mediaAnalysis.PrimaryAudioStream!.ChannelLayout);
|
||||
}
|
||||
|
||||
[TestMethod, Timeout(10000)]
|
||||
|
|
|
@ -13,7 +13,7 @@ public void TestInitialize()
|
|||
|
||||
{
|
||||
// After testing reset global configuration to null, to be not wrong for other test relying on configuration
|
||||
typeof(GlobalFFOptions).GetField("_current", BindingFlags.NonPublic | BindingFlags.Static).SetValue(GlobalFFOptions.Current, null);
|
||||
typeof(GlobalFFOptions).GetField("_current", BindingFlags.NonPublic | BindingFlags.Static)!.SetValue(GlobalFFOptions.Current, null);
|
||||
}
|
||||
|
||||
private static FFMpegArgumentProcessor CreateArgumentProcessor() => FFMpegArguments
|
||||
|
@ -69,10 +69,6 @@ public void Processor_Options_CanBeOverridden_And_Configured()
|
|||
[TestMethod]
|
||||
public void Options_Global_And_Session_Options_Can_Differ()
|
||||
{
|
||||
FFMpegArgumentProcessor CreateArgumentProcessor() => FFMpegArguments
|
||||
.FromFileInput("")
|
||||
.OutputToFile("");
|
||||
|
||||
var globalWorkingDir = "Whatever";
|
||||
GlobalFFOptions.Configure(new FFOptions { WorkingDirectory = globalWorkingDir });
|
||||
|
||||
|
|
|
@ -2,16 +2,31 @@
|
|||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
|
||||
<Nullable>disable</Nullable>
|
||||
|
||||
<LangVersion>default</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="ffmpeg.config.json" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.9.0" />
|
||||
<PackageReference Include="GitHubActionsTestLogger" Version="2.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.0.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FFMpegCore.Extensions.System.Drawing.Common\FFMpegCore.Extensions.System.Drawing.Common.csproj" />
|
||||
<ProjectReference Include="..\FFMpegCore\FFMpegCore.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="ffmpeg.config.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<None Update="Resources\input.webm">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
|
@ -28,69 +43,44 @@
|
|||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Resources\audio.raw">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="ffmpeg.config.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.3.0" />
|
||||
<PackageReference Include="GitHubActionsTestLogger" Version="1.2.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="2.2.8" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="2.2.8" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FFMpegCore\FFMpegCore.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
</None>
|
||||
<None Update="Resources\audio.mp3">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Resources\audio_only.mp4">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Resources\cover.png">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Resources\images\a.png">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Resources\images\b.png">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Resources\images\c.png">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Resources\images\d.png">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Resources\images\e.png">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Resources\images\f.png">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Resources\input.mp4">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Resources\mute.mp4">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Resources\sample.srt">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Properties\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -99,7 +99,7 @@ public void MediaAnalysis_ParseDuration(string duration, int expectedDays, int e
|
|||
Assert.AreEqual(expectedMilliseconds, parsedDuration.Milliseconds);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestMethod, Ignore("Consistently fails on GitHub Workflow ubuntu agents")]
|
||||
public async Task Uri_Duration()
|
||||
{
|
||||
var fileAnalysis = await FFProbe.AnalyseAsync(new Uri("https://github.com/rosenbjerg/FFMpegCore/raw/master/FFMpegCore.Test/Resources/input_3sec.webm"));
|
||||
|
|
|
@ -1,14 +1,7 @@
|
|||
using FFMpegCore.Builders.MetaData;
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace FFMpegCore.Test
|
||||
{
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
using FFMpegCore.Extend;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Imaging;
|
||||
using System.Numerics;
|
||||
using System.Runtime.Versioning;
|
||||
using FFMpegCore.Extensions.System.Drawing.Common;
|
||||
using FFMpegCore.Pipes;
|
||||
|
||||
namespace FFMpegCore.Test
|
||||
namespace FFMpegCore.Test.Utilities
|
||||
{
|
||||
[SupportedOSPlatform("windows")]
|
||||
static class BitmapSource
|
||||
{
|
||||
public static IEnumerable<IVideoFrame> CreateBitmaps(int count, PixelFormat fmt, int w, int h)
|
||||
|
|
23
FFMpegCore.Test/Utilities/WindowsOnlyDataTestMethod.cs
Normal file
23
FFMpegCore.Test/Utilities/WindowsOnlyDataTestMethod.cs
Normal file
|
@ -0,0 +1,23 @@
|
|||
using System.Runtime.InteropServices;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace FFMpegCore.Test.Utilities;
|
||||
|
||||
public class WindowsOnlyDataTestMethod : DataTestMethodAttribute
|
||||
{
|
||||
public override TestResult[] Execute(ITestMethod testMethod)
|
||||
{
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
var message = $"Test not executed on other platforms than Windows";
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new TestResult { Outcome = UnitTestOutcome.Inconclusive, TestFailureException = new AssertInconclusiveException(message) }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return base.Execute(testMethod);
|
||||
}
|
||||
}
|
23
FFMpegCore.Test/Utilities/WindowsOnlyTestMethod.cs
Normal file
23
FFMpegCore.Test/Utilities/WindowsOnlyTestMethod.cs
Normal file
|
@ -0,0 +1,23 @@
|
|||
using System.Runtime.InteropServices;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace FFMpegCore.Test.Utilities;
|
||||
|
||||
public class WindowsOnlyTestMethod : TestMethodAttribute
|
||||
{
|
||||
public override TestResult[] Execute(ITestMethod testMethod)
|
||||
{
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
var message = $"Test not executed on other platforms than Windows";
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new TestResult { Outcome = UnitTestOutcome.Inconclusive, TestFailureException = new AssertInconclusiveException(message) }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return base.Execute(testMethod);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,7 @@
|
|||
using FFMpegCore.Enums;
|
||||
using FFMpegCore.Arguments;
|
||||
using FFMpegCore.Enums;
|
||||
using FFMpegCore.Exceptions;
|
||||
using FFMpegCore.Pipes;
|
||||
using FFMpegCore.Test.Resources;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using System;
|
||||
|
@ -7,12 +10,12 @@
|
|||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using FFMpegCore.Arguments;
|
||||
using FFMpegCore.Exceptions;
|
||||
using FFMpegCore.Pipes;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FFMpegCore.Extensions.System.Drawing.Common;
|
||||
using FFMpegCore.Test.Utilities;
|
||||
|
||||
namespace FFMpegCore.Test
|
||||
{
|
||||
|
@ -85,7 +88,8 @@ public void Video_ToH265_MKV_Args()
|
|||
Assert.IsTrue(success);
|
||||
}
|
||||
|
||||
[DataTestMethod, Timeout(10000)]
|
||||
[SupportedOSPlatform("windows")]
|
||||
[WindowsOnlyDataTestMethod, Timeout(10000)]
|
||||
[DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)]
|
||||
[DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)]
|
||||
public void Video_ToMP4_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat)
|
||||
|
@ -101,7 +105,8 @@ public void Video_ToMP4_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat
|
|||
Assert.IsTrue(success);
|
||||
}
|
||||
|
||||
[TestMethod, Timeout(10000)]
|
||||
[SupportedOSPlatform("windows")]
|
||||
[WindowsOnlyTestMethod, Timeout(10000)]
|
||||
public void Video_ToMP4_Args_Pipe_DifferentImageSizes()
|
||||
{
|
||||
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
|
||||
|
@ -120,8 +125,8 @@ public void Video_ToMP4_Args_Pipe_DifferentImageSizes()
|
|||
.ProcessSynchronously());
|
||||
}
|
||||
|
||||
|
||||
[TestMethod, Timeout(10000)]
|
||||
[SupportedOSPlatform("windows")]
|
||||
[WindowsOnlyTestMethod, Timeout(10000)]
|
||||
public async Task Video_ToMP4_Args_Pipe_DifferentImageSizes_Async()
|
||||
{
|
||||
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
|
||||
|
@ -140,7 +145,8 @@ public async Task Video_ToMP4_Args_Pipe_DifferentImageSizes_Async()
|
|||
.ProcessAsynchronously());
|
||||
}
|
||||
|
||||
[TestMethod, Timeout(10000)]
|
||||
[SupportedOSPlatform("windows")]
|
||||
[WindowsOnlyTestMethod, Timeout(10000)]
|
||||
public void Video_ToMP4_Args_Pipe_DifferentPixelFormats()
|
||||
{
|
||||
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
|
||||
|
@ -159,8 +165,8 @@ public void Video_ToMP4_Args_Pipe_DifferentPixelFormats()
|
|||
.ProcessSynchronously());
|
||||
}
|
||||
|
||||
|
||||
[TestMethod, Timeout(10000)]
|
||||
[SupportedOSPlatform("windows")]
|
||||
[WindowsOnlyTestMethod, Timeout(10000)]
|
||||
public async Task Video_ToMP4_Args_Pipe_DifferentPixelFormats_Async()
|
||||
{
|
||||
using var outputFile = new TemporaryFile($"out{VideoType.Mp4.Extension}");
|
||||
|
@ -314,7 +320,8 @@ public void Video_ToTS_Args()
|
|||
Assert.IsTrue(success);
|
||||
}
|
||||
|
||||
[DataTestMethod, Timeout(10000)]
|
||||
[SupportedOSPlatform("windows")]
|
||||
[WindowsOnlyDataTestMethod, Timeout(10000)]
|
||||
[DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)]
|
||||
[DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)]
|
||||
public async Task Video_ToTS_Args_Pipe(System.Drawing.Imaging.PixelFormat pixelFormat)
|
||||
|
@ -346,7 +353,8 @@ public async Task Video_ToOGV_Resize()
|
|||
Assert.IsTrue(success);
|
||||
}
|
||||
|
||||
[DataTestMethod, Timeout(10000)]
|
||||
[SupportedOSPlatform("windows")]
|
||||
[WindowsOnlyDataTestMethod, Timeout(10000)]
|
||||
[DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)]
|
||||
[DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)]
|
||||
// [DataRow(PixelFormat.Format48bppRgb)]
|
||||
|
@ -381,7 +389,8 @@ public void Scale_Mp4_Multithreaded()
|
|||
Assert.IsTrue(success);
|
||||
}
|
||||
|
||||
[DataTestMethod, Timeout(10000)]
|
||||
[SupportedOSPlatform("windows")]
|
||||
[WindowsOnlyDataTestMethod, Timeout(10000)]
|
||||
[DataRow(System.Drawing.Imaging.PixelFormat.Format24bppRgb)]
|
||||
[DataRow(System.Drawing.Imaging.PixelFormat.Format32bppArgb)]
|
||||
// [DataRow(PixelFormat.Format48bppRgb)]
|
||||
|
@ -398,18 +407,20 @@ public void Video_ToMP4_Resize_Args_Pipe(System.Drawing.Imaging.PixelFormat pixe
|
|||
Assert.IsTrue(success);
|
||||
}
|
||||
|
||||
[TestMethod, Timeout(10000)]
|
||||
[SupportedOSPlatform("windows")]
|
||||
[WindowsOnlyTestMethod, Timeout(10000)]
|
||||
public void Video_Snapshot_InMemory()
|
||||
{
|
||||
var input = FFProbe.Analyse(TestResources.Mp4Video);
|
||||
using var bitmap = FFMpeg.Snapshot(TestResources.Mp4Video);
|
||||
using var bitmap = FFMpegImage.Snapshot(TestResources.Mp4Video);
|
||||
|
||||
Assert.AreEqual(input.PrimaryVideoStream!.Width, bitmap.Width);
|
||||
Assert.AreEqual(input.PrimaryVideoStream.Height, bitmap.Height);
|
||||
Assert.AreEqual(bitmap.RawFormat, ImageFormat.Png);
|
||||
}
|
||||
|
||||
[TestMethod, Timeout(10000)]
|
||||
[SupportedOSPlatform("windows")]
|
||||
[WindowsOnlyTestMethod, Timeout(10000)]
|
||||
public void Video_Snapshot_PersistSnapshot()
|
||||
{
|
||||
var outputPath = new TemporaryFile("out.png");
|
||||
|
@ -445,7 +456,7 @@ public void Video_Join()
|
|||
Assert.AreEqual(input.PrimaryVideoStream.Width, result.PrimaryVideoStream.Width);
|
||||
}
|
||||
|
||||
[TestMethod, Timeout(10000)]
|
||||
[WindowsOnlyTestMethod, Timeout(10000)]
|
||||
public void Video_Join_Image_Sequence()
|
||||
{
|
||||
var imageSet = new List<ImageInfo>();
|
||||
|
@ -460,8 +471,8 @@ public void Video_Join_Image_Sequence()
|
|||
}
|
||||
});
|
||||
|
||||
using var outputFile = new TemporaryFile("out.mp4");
|
||||
var success = FFMpeg.JoinImageSequence(outputFile, images: imageSet.ToArray());
|
||||
var outputFile = new TemporaryFile("out.mp4");
|
||||
var success = FFMpegImage.JoinImageSequence(outputFile, images: imageSet.ToArray());
|
||||
Assert.IsTrue(success);
|
||||
var result = FFProbe.Analyse(outputFile);
|
||||
Assert.AreEqual(3, result.Duration.Seconds);
|
||||
|
@ -505,14 +516,22 @@ public void Video_UpdatesProgress()
|
|||
|
||||
var percentageDone = 0.0;
|
||||
var timeDone = TimeSpan.Zero;
|
||||
void OnPercentageProgess(double percentage) => percentageDone = percentage;
|
||||
void OnTimeProgess(TimeSpan time) => timeDone = time;
|
||||
|
||||
var analysis = FFProbe.Analyse(TestResources.Mp4Video);
|
||||
|
||||
void OnPercentageProgess(double percentage)
|
||||
{
|
||||
if (percentage < 100) percentageDone = percentage;
|
||||
}
|
||||
|
||||
void OnTimeProgess(TimeSpan time)
|
||||
{
|
||||
if (time < analysis.Duration) timeDone = time;
|
||||
}
|
||||
|
||||
var success = FFMpegArguments
|
||||
.FromFileInput(TestResources.Mp4Video)
|
||||
.OutputToFile(outputFile, false, opt => opt
|
||||
.WithDuration(TimeSpan.FromSeconds(2)))
|
||||
.WithDuration(analysis.Duration))
|
||||
.NotifyOnProgress(OnPercentageProgess, analysis.Duration)
|
||||
.NotifyOnProgress(OnTimeProgess)
|
||||
.ProcessSynchronously();
|
||||
|
@ -520,7 +539,9 @@ public void Video_UpdatesProgress()
|
|||
Assert.IsTrue(success);
|
||||
Assert.IsTrue(File.Exists(outputFile));
|
||||
Assert.AreNotEqual(0.0, percentageDone);
|
||||
Assert.AreNotEqual(100.0, percentageDone);
|
||||
Assert.AreNotEqual(TimeSpan.Zero, timeDone);
|
||||
Assert.AreNotEqual(analysis.Duration, timeDone);
|
||||
}
|
||||
|
||||
[TestMethod, Timeout(10000)]
|
||||
|
@ -544,7 +565,8 @@ public void Video_OutputsData()
|
|||
Assert.IsTrue(File.Exists(outputFile));
|
||||
}
|
||||
|
||||
[TestMethod, Timeout(10000)]
|
||||
[SupportedOSPlatform("windows")]
|
||||
[WindowsOnlyTestMethod, Timeout(10000)]
|
||||
public void Video_TranscodeInMemory()
|
||||
{
|
||||
using var resStream = new MemoryStream();
|
||||
|
|
|
@ -9,6 +9,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMpegCore.Test", "FFMpegCo
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMpegCore.Examples", "FFMpegCore.Examples\FFMpegCore.Examples.csproj", "{3125CF91-FFBD-4E4E-8930-247116AFE772}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FFMpegCore.Extensions.System.Drawing.Common", "FFMpegCore.Extensions.System.Drawing.Common\FFMpegCore.Extensions.System.Drawing.Common.csproj", "{9C1A4930-9369-4A18-AD98-929A2A510D80}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
@ -27,6 +29,10 @@ Global
|
|||
{3125CF91-FFBD-4E4E-8930-247116AFE772}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3125CF91-FFBD-4E4E-8930-247116AFE772}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3125CF91-FFBD-4E4E-8930-247116AFE772}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{9C1A4930-9369-4A18-AD98-929A2A510D80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9C1A4930-9369-4A18-AD98-929A2A510D80}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9C1A4930-9369-4A18-AD98-929A2A510D80}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9C1A4930-9369-4A18-AD98-929A2A510D80}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace FFMpegCore.Extend
|
||||
|
|
|
@ -4,10 +4,10 @@ public class AudibleEncryptionKeyArgument : IArgument
|
|||
{
|
||||
private readonly bool _aaxcMode;
|
||||
|
||||
private readonly string _key;
|
||||
private readonly string _iv;
|
||||
private readonly string? _key;
|
||||
private readonly string? _iv;
|
||||
|
||||
private readonly string _activationBytes;
|
||||
private readonly string? _activationBytes;
|
||||
|
||||
|
||||
public AudibleEncryptionKeyArgument(string activationBytes)
|
||||
|
|
|
@ -34,8 +34,8 @@ private string GetText()
|
|||
|
||||
public interface IAudioFilterArgument
|
||||
{
|
||||
public string Key { get; }
|
||||
public string Value { get; }
|
||||
string Key { get; }
|
||||
string Value { get; }
|
||||
}
|
||||
|
||||
public class AudioFilterOptions
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace FFMpegCore.Arguments
|
||||
{
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
using FFMpegCore.Extend;
|
||||
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
using FFMpegCore.Extend;
|
||||
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
using FFMpegCore.Enums;
|
||||
using FFMpegCore.Exceptions;
|
||||
using FFMpegCore.Helpers;
|
||||
using FFMpegCore.Pipes;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
|
@ -12,102 +11,9 @@
|
|||
|
||||
namespace FFMpegCore
|
||||
{
|
||||
public static class FFMpeg
|
||||
public static class SnapshotArgumentBuilder
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves a 'png' thumbnail from the input video to drive
|
||||
/// </summary>
|
||||
/// <param name="input">Source video analysis</param>
|
||||
/// <param name="output">Output video file path</param>
|
||||
/// <param name="captureTime">Seek position where the thumbnail should be taken.</param>
|
||||
/// <param name="size">Thumbnail size. If width or height equal 0, the other will be computed automatically.</param>
|
||||
/// <param name="streamIndex">Selected video stream index.</param>
|
||||
/// <param name="inputFileIndex">Input file index</param>
|
||||
/// <returns>Bitmap with the requested snapshot.</returns>
|
||||
public static bool Snapshot(string input, string output, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0)
|
||||
{
|
||||
if (Path.GetExtension(output) != FileExtension.Png)
|
||||
output = Path.GetFileNameWithoutExtension(output) + FileExtension.Png;
|
||||
|
||||
var source = FFProbe.Analyse(input);
|
||||
var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex);
|
||||
|
||||
return arguments
|
||||
.OutputToFile(output, true, outputOptions)
|
||||
.ProcessSynchronously();
|
||||
}
|
||||
/// <summary>
|
||||
/// Saves a 'png' thumbnail from the input video to drive
|
||||
/// </summary>
|
||||
/// <param name="input">Source video analysis</param>
|
||||
/// <param name="output">Output video file path</param>
|
||||
/// <param name="captureTime">Seek position where the thumbnail should be taken.</param>
|
||||
/// <param name="size">Thumbnail size. If width or height equal 0, the other will be computed automatically.</param>
|
||||
/// <param name="streamIndex">Selected video stream index.</param>
|
||||
/// <param name="inputFileIndex">Input file index</param>
|
||||
/// <returns>Bitmap with the requested snapshot.</returns>
|
||||
public static async Task<bool> SnapshotAsync(string input, string output, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0)
|
||||
{
|
||||
if (Path.GetExtension(output) != FileExtension.Png)
|
||||
output = Path.GetFileNameWithoutExtension(output) + FileExtension.Png;
|
||||
|
||||
var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false);
|
||||
var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex);
|
||||
|
||||
return await arguments
|
||||
.OutputToFile(output, true, outputOptions)
|
||||
.ProcessAsynchronously();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves a 'png' thumbnail to an in-memory bitmap
|
||||
/// </summary>
|
||||
/// <param name="input">Source video file.</param>
|
||||
/// <param name="captureTime">Seek position where the thumbnail should be taken.</param>
|
||||
/// <param name="size">Thumbnail size. If width or height equal 0, the other will be computed automatically.</param>
|
||||
/// <param name="streamIndex">Selected video stream index.</param>
|
||||
/// <param name="inputFileIndex">Input file index</param>
|
||||
/// <returns>Bitmap with the requested snapshot.</returns>
|
||||
public static Bitmap Snapshot(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0)
|
||||
{
|
||||
var source = FFProbe.Analyse(input);
|
||||
var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex);
|
||||
using var ms = new MemoryStream();
|
||||
|
||||
arguments
|
||||
.OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options
|
||||
.ForceFormat("rawvideo")))
|
||||
.ProcessSynchronously();
|
||||
|
||||
ms.Position = 0;
|
||||
using var bitmap = new Bitmap(ms);
|
||||
return bitmap.Clone(new Rectangle(0, 0, bitmap.Width, bitmap.Height), bitmap.PixelFormat);
|
||||
}
|
||||
/// <summary>
|
||||
/// Saves a 'png' thumbnail to an in-memory bitmap
|
||||
/// </summary>
|
||||
/// <param name="input">Source video file.</param>
|
||||
/// <param name="captureTime">Seek position where the thumbnail should be taken.</param>
|
||||
/// <param name="size">Thumbnail size. If width or height equal 0, the other will be computed automatically.</param>
|
||||
/// <param name="streamIndex">Selected video stream index.</param>
|
||||
/// <param name="inputFileIndex">Input file index</param>
|
||||
/// <returns>Bitmap with the requested snapshot.</returns>
|
||||
public static async Task<Bitmap> SnapshotAsync(string input, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0)
|
||||
{
|
||||
var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false);
|
||||
var (arguments, outputOptions) = BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex);
|
||||
using var ms = new MemoryStream();
|
||||
|
||||
await arguments
|
||||
.OutputToPipe(new StreamPipeSink(ms), options => outputOptions(options
|
||||
.ForceFormat("rawvideo")))
|
||||
.ProcessAsynchronously();
|
||||
|
||||
ms.Position = 0;
|
||||
return new Bitmap(ms);
|
||||
}
|
||||
|
||||
private static (FFMpegArguments, Action<FFMpegArgumentOptions> outputOptions) BuildSnapshotArguments(
|
||||
public static (FFMpegArguments, Action<FFMpegArgumentOptions> outputOptions) BuildSnapshotArguments(
|
||||
string input,
|
||||
IMediaAnalysis source,
|
||||
Size? size = null,
|
||||
|
@ -157,13 +63,61 @@ private static (FFMpegArguments, Action<FFMpegArgumentOptions> outputOptions) Bu
|
|||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
public static class FFMpeg
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves a 'png' thumbnail from the input video to drive
|
||||
/// </summary>
|
||||
/// <param name="input">Source video analysis</param>
|
||||
/// <param name="output">Output video file path</param>
|
||||
/// <param name="captureTime">Seek position where the thumbnail should be taken.</param>
|
||||
/// <param name="size">Thumbnail size. If width or height equal 0, the other will be computed automatically.</param>
|
||||
/// <param name="streamIndex">Selected video stream index.</param>
|
||||
/// <param name="inputFileIndex">Input file index</param>
|
||||
/// <returns>Bitmap with the requested snapshot.</returns>
|
||||
public static bool Snapshot(string input, string output, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0)
|
||||
{
|
||||
if (Path.GetExtension(output) != FileExtension.Png)
|
||||
output = Path.GetFileNameWithoutExtension(output) + FileExtension.Png;
|
||||
|
||||
var source = FFProbe.Analyse(input);
|
||||
var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex);
|
||||
|
||||
return arguments
|
||||
.OutputToFile(output, true, outputOptions)
|
||||
.ProcessSynchronously();
|
||||
}
|
||||
/// <summary>
|
||||
/// Saves a 'png' thumbnail from the input video to drive
|
||||
/// </summary>
|
||||
/// <param name="input">Source video analysis</param>
|
||||
/// <param name="output">Output video file path</param>
|
||||
/// <param name="captureTime">Seek position where the thumbnail should be taken.</param>
|
||||
/// <param name="size">Thumbnail size. If width or height equal 0, the other will be computed automatically.</param>
|
||||
/// <param name="streamIndex">Selected video stream index.</param>
|
||||
/// <param name="inputFileIndex">Input file index</param>
|
||||
/// <returns>Bitmap with the requested snapshot.</returns>
|
||||
public static async Task<bool> SnapshotAsync(string input, string output, Size? size = null, TimeSpan? captureTime = null, int? streamIndex = null, int inputFileIndex = 0)
|
||||
{
|
||||
if (Path.GetExtension(output) != FileExtension.Png)
|
||||
output = Path.GetFileNameWithoutExtension(output) + FileExtension.Png;
|
||||
|
||||
var source = await FFProbe.AnalyseAsync(input).ConfigureAwait(false);
|
||||
var (arguments, outputOptions) = SnapshotArgumentBuilder.BuildSnapshotArguments(input, source, size, captureTime, streamIndex, inputFileIndex);
|
||||
|
||||
return await arguments
|
||||
.OutputToFile(output, true, outputOptions)
|
||||
.ProcessAsynchronously();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Convert a video do a different format.
|
||||
/// </summary>
|
||||
/// <param name="source">Input video source.</param>
|
||||
/// <param name="input">Input video source.</param>
|
||||
/// <param name="output">Output information.</param>
|
||||
/// <param name="type">Target conversion video type.</param>
|
||||
/// <param name="format">Target conversion video format.</param>
|
||||
/// <param name="speed">Conversion target speed/quality (faster speed = lower quality).</param>
|
||||
/// <param name="size">Video size.</param>
|
||||
/// <param name="audioQuality">Conversion target audio quality.</param>
|
||||
|
@ -237,35 +191,6 @@ public static bool Convert(
|
|||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a poster image to an audio file.
|
||||
/// </summary>
|
||||
/// <param name="image">Source image file.</param>
|
||||
/// <param name="audio">Source audio file.</param>
|
||||
/// <param name="output">Output video file.</param>
|
||||
/// <returns></returns>
|
||||
public static bool PosterWithAudio(string image, string audio, string output)
|
||||
{
|
||||
FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.Mp4);
|
||||
using (var imageFile = Image.FromFile(image))
|
||||
{
|
||||
FFMpegHelper.ConversionSizeExceptionCheck(imageFile);
|
||||
}
|
||||
|
||||
return FFMpegArguments
|
||||
.FromFileInput(image, false, options => options
|
||||
.Loop(1)
|
||||
.ForceFormat("image2"))
|
||||
.AddFileInput(audio)
|
||||
.OutputToFile(output, true, options => options
|
||||
.ForcePixelFormat("yuv420p")
|
||||
.WithVideoCodec(VideoCodec.LibX264)
|
||||
.WithConstantRateFactor(21)
|
||||
.WithAudioBitrate(AudioQuality.Normal)
|
||||
.UsingShortest())
|
||||
.ProcessSynchronously();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Joins a list of video files.
|
||||
/// </summary>
|
||||
|
@ -299,44 +224,6 @@ public static bool Join(string output, params string[] videos)
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an image sequence to a video.
|
||||
/// </summary>
|
||||
/// <param name="output">Output video file.</param>
|
||||
/// <param name="frameRate">FPS</param>
|
||||
/// <param name="images">Image sequence collection</param>
|
||||
/// <returns>Output video information.</returns>
|
||||
public static bool JoinImageSequence(string output, double frameRate = 30, params ImageInfo[] images)
|
||||
{
|
||||
var tempFolderName = Path.Combine(GlobalFFOptions.Current.TemporaryFilesFolder, Guid.NewGuid().ToString());
|
||||
var temporaryImageFiles = images.Select((imageInfo, index) =>
|
||||
{
|
||||
using var image = Image.FromFile(imageInfo.FullName);
|
||||
FFMpegHelper.ConversionSizeExceptionCheck(image);
|
||||
var destinationPath = Path.Combine(tempFolderName, $"{index.ToString().PadLeft(9, '0')}{imageInfo.Extension}");
|
||||
Directory.CreateDirectory(tempFolderName);
|
||||
File.Copy(imageInfo.FullName, destinationPath);
|
||||
return destinationPath;
|
||||
}).ToArray();
|
||||
|
||||
var firstImage = images.First();
|
||||
try
|
||||
{
|
||||
return FFMpegArguments
|
||||
.FromFileInput(Path.Combine(tempFolderName, "%09d.png"), false)
|
||||
.OutputToFile(output, true, options => options
|
||||
.ForcePixelFormat("yuv420p")
|
||||
.Resize(firstImage.Width, firstImage.Height)
|
||||
.WithFramerate(frameRate))
|
||||
.ProcessSynchronously();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Cleanup(temporaryImageFiles);
|
||||
Directory.Delete(tempFolderName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records M3U8 streams to the specified output.
|
||||
/// </summary>
|
||||
|
@ -445,15 +332,15 @@ public static IReadOnlyList<PixelFormat> GetPixelFormats()
|
|||
return FFMpegCache.PixelFormats.Values.ToList().AsReadOnly();
|
||||
}
|
||||
|
||||
public static bool TryGetPixelFormat(string name, out PixelFormat fmt)
|
||||
public static bool TryGetPixelFormat(string name, out PixelFormat format)
|
||||
{
|
||||
if (!GlobalFFOptions.Current.UseCache)
|
||||
{
|
||||
fmt = GetPixelFormatsInternal().FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim());
|
||||
return fmt != null;
|
||||
format = GetPixelFormatsInternal().FirstOrDefault(x => x.Name == name.ToLowerInvariant().Trim());
|
||||
return format != null;
|
||||
}
|
||||
else
|
||||
return FFMpegCache.PixelFormats.TryGetValue(name, out fmt);
|
||||
return FFMpegCache.PixelFormats.TryGetValue(name, out format);
|
||||
}
|
||||
|
||||
public static PixelFormat GetPixelFormat(string name)
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
using System;
|
||||
using FFMpegCore.Exceptions;
|
||||
using FFMpegCore.Helpers;
|
||||
using Instances;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FFMpegCore.Exceptions;
|
||||
using FFMpegCore.Helpers;
|
||||
using Instances;
|
||||
|
||||
namespace FFMpegCore
|
||||
{
|
||||
|
@ -204,10 +204,10 @@ private ProcessArguments PrepareProcessArguments(FFOptions ffOptions,
|
|||
var processArguments = new ProcessArguments(startInfo);
|
||||
cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
if (_onOutput != null || _onTimeProgress != null || (_onPercentageProgress != null && _totalTimespan != null))
|
||||
if (_onOutput != null)
|
||||
processArguments.OutputDataReceived += OutputData;
|
||||
|
||||
if (_onError != null)
|
||||
if (_onError != null || _onTimeProgress != null || (_onPercentageProgress != null && _totalTimespan != null))
|
||||
processArguments.ErrorDataReceived += ErrorData;
|
||||
|
||||
return processArguments;
|
||||
|
@ -216,12 +216,6 @@ private ProcessArguments PrepareProcessArguments(FFOptions ffOptions,
|
|||
private void ErrorData(object sender, string msg)
|
||||
{
|
||||
_onError?.Invoke(msg);
|
||||
}
|
||||
|
||||
private void OutputData(object sender, string msg)
|
||||
{
|
||||
Debug.WriteLine(msg);
|
||||
_onOutput?.Invoke(msg);
|
||||
|
||||
var match = ProgressRegex.Match(msg);
|
||||
if (!match.Success) return;
|
||||
|
@ -233,5 +227,11 @@ private void OutputData(object sender, string msg)
|
|||
var percentage = Math.Round(processed.TotalSeconds / _totalTimespan.Value.TotalSeconds * 100, 2);
|
||||
_onPercentageProgress(percentage);
|
||||
}
|
||||
|
||||
private void OutputData(object sender, string msg)
|
||||
{
|
||||
Debug.WriteLine(msg);
|
||||
_onOutput?.Invoke(msg);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
|
|
|
@ -1,45 +1,23 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
<RepositoryUrl>https://github.com/rosenbjerg/FFMpegCore</RepositoryUrl>
|
||||
<PackageProjectUrl>https://github.com/rosenbjerg/FFMpegCore</PackageProjectUrl>
|
||||
<Copyright></Copyright>
|
||||
<Description>A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications</Description>
|
||||
<AssemblyVersion>4.0.0.0</AssemblyVersion>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageReleaseNotes>- Fixes for `MetaDataArgument` (thanks @JKamsker)
|
||||
- Support for Audible `aaxc` (thanks @JKamsker)
|
||||
- Include errordata in `IMediaAnalysis` (thanks @JKamsker)
|
||||
- Pass `FFOptions` properly when using ffprobe (thanks @Notheisz57)
|
||||
- CancellationToken support for `AnalyseAsync`
|
||||
- Case-insensitive dictionaries for `Tags` and `Disposition`
|
||||
- Fix for `PosterWithAudio`
|
||||
- Fix for `JoinImageSequence`
|
||||
- Updates to dependendies
|
||||
- A lot of bug fixes</PackageReleaseNotes>
|
||||
<LangVersion>8</LangVersion>
|
||||
<PackageVersion>4.8.0</PackageVersion>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<Authors>Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev</Authors>
|
||||
<PackageTags>ffmpeg ffprobe convert video audio mediafile resize analyze muxing</PackageTags>
|
||||
<RepositoryType>GitHub</RepositoryType>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<Nullable>enable</Nullable>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<IsPackable>true</IsPackable>
|
||||
<Description>A .NET Standard FFMpeg/FFProbe wrapper for easily integrating media analysis and conversion into your .NET applications</Description>
|
||||
<PackageVersion>5.0.0</PackageVersion>
|
||||
<PackageReleaseNotes>
|
||||
</PackageReleaseNotes>
|
||||
<PackageTags>ffmpeg ffprobe convert video audio mediafile resize analyze muxing</PackageTags>
|
||||
<Authors>Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev</Authors>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="FFMPEG\bin\**\*">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<None Include="..\README.md" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\README.md" Pack="true" PackagePath="\"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Instances" Version="2.0.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="5.0.2" />
|
||||
<PackageReference Include="System.Text.Json" Version="6.0.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Instances" Version="3.0.0"/>
|
||||
<PackageReference Include="System.Text.Json" Version="7.0.1"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -12,7 +12,7 @@ public class FFProbeAnalysis
|
|||
public Format Format { get; set; } = null!;
|
||||
|
||||
[JsonIgnore]
|
||||
public IReadOnlyList<string> ErrorData { get; set; }
|
||||
public IReadOnlyList<string> ErrorData { get; set; } = new List<string>();
|
||||
}
|
||||
|
||||
public class FFProbeStream : ITagsContainer, IDispositionContainer
|
||||
|
@ -108,7 +108,7 @@ public class Format : ITagsContainer
|
|||
public string Size { get; set; } = null!;
|
||||
|
||||
[JsonPropertyName("bit_rate")]
|
||||
public string BitRate { get; set; } = null!;
|
||||
public string? BitRate { get; set; } = null!;
|
||||
|
||||
[JsonPropertyName("probe_score")]
|
||||
public int ProbeScore { get; set; }
|
||||
|
|
|
@ -13,7 +13,7 @@ internal MediaAnalysis(FFProbeAnalysis analysis)
|
|||
VideoStreams = analysis.Streams.Where(stream => stream.CodecType == "video").Select(ParseVideoStream).ToList();
|
||||
AudioStreams = analysis.Streams.Where(stream => stream.CodecType == "audio").Select(ParseAudioStream).ToList();
|
||||
SubtitleStreams = analysis.Streams.Where(stream => stream.CodecType == "subtitle").Select(ParseSubtitleStream).ToList();
|
||||
ErrorData = analysis.ErrorData ?? new List<string>().AsReadOnly();
|
||||
ErrorData = analysis.ErrorData;
|
||||
}
|
||||
|
||||
private MediaFormat ParseFormat(Format analysisFormat)
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using FFMpegCore.Exceptions;
|
||||
using Instances;
|
||||
|
@ -10,13 +9,10 @@ public static class FFMpegHelper
|
|||
{
|
||||
private static bool _ffmpegVerified;
|
||||
|
||||
public static void ConversionSizeExceptionCheck(Image image)
|
||||
=> ConversionSizeExceptionCheck(image.Size.Width, image.Size.Height);
|
||||
|
||||
public static void ConversionSizeExceptionCheck(IMediaAnalysis info)
|
||||
=> ConversionSizeExceptionCheck(info.PrimaryVideoStream!.Width, info.PrimaryVideoStream.Height);
|
||||
|
||||
private static void ConversionSizeExceptionCheck(int width, int height)
|
||||
public static void ConversionSizeExceptionCheck(int width, int height)
|
||||
{
|
||||
if (height % 2 != 0 || width % 2 != 0 )
|
||||
throw new ArgumentException("FFMpeg yuv420p encoding requires the width and height to be a multiple of 2!");
|
||||
|
|
Loading…
Reference in a new issue