diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..bdb0cab --- /dev/null +++ b/.gitattributes @@ -0,0 +1,17 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Custom for Visual Studio +*.cs diff=csharp + +# Standard to msysgit +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1298f0d --- /dev/null +++ b/.gitignore @@ -0,0 +1,343 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ +# ASP.NET Core default setup: bower directory is configured as wwwroot/lib/ and bower restore is true +**/wwwroot/lib/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +!FFMpegCore/FFMPEG/bin/**/* \ No newline at end of file diff --git a/.nuget/nuget.exe b/.nuget/nuget.exe new file mode 100644 index 0000000..ccb2979 Binary files /dev/null and b/.nuget/nuget.exe differ diff --git a/FFMpegCore.Test/ArgumentBuilderTests.cs b/FFMpegCore.Test/ArgumentBuilderTests.cs new file mode 100644 index 0000000..080eef4 --- /dev/null +++ b/FFMpegCore.Test/ArgumentBuilderTests.cs @@ -0,0 +1,223 @@ +using FFMpegCore.FFMPEG.Arguments; +using FFMpegCore.FFMPEG.Enums; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FFMpegCore.Tests +{ + [TestClass] + public class ArgumentBuilderTests : BaseTest + { + List concatFiles = new List + { "1.mp4", "2.mp4", "3.mp4", "4.mp4"}; + + FFArgumentBuilder builder; + + public ArgumentBuilderTests() : base() + { + builder = new FFArgumentBuilder(); + } + + private string GetArgumentsString(params Argument[] args) + { + var container = new ArgumentsContainer(); + container.Add(new OutputArgument("output.mp4")); + container.Add(new InputArgument("input.mp4")); + + foreach (var a in args) + { + container.Add(a); + } + + return builder.BuildArguments(container); + } + + + [TestMethod] + public void Builder_BuildString_IO_1() + { + var str = GetArgumentsString(); + + Assert.IsTrue(str == "-i \"input.mp4\" \"output.mp4\""); + } + + [TestMethod] + public void Builder_BuildString_Scale() + { + var str = GetArgumentsString(new ScaleArgument(VideoSize.Hd)); + + Assert.IsTrue(str == "-i \"input.mp4\" -vf scale=-1:720 \"output.mp4\""); + } + + [TestMethod] + public void Builder_BuildString_AudioCodec() + { + var str = GetArgumentsString(new AudioCodecArgument(AudioCodec.Aac, AudioQuality.Normal)); + + Assert.IsTrue(str == "-i \"input.mp4\" -codec:a aac -b:a 128k -strict experimental \"output.mp4\""); + } + + [TestMethod] + public void Builder_BuildString_BitStream() + { + var str = GetArgumentsString(new BitStreamFilterArgument(Channel.Audio, Filter.H264_Mp4ToAnnexB)); + + Assert.IsTrue(str == "-i \"input.mp4\" -bsf:a h264_mp4toannexb \"output.mp4\""); + } + + [TestMethod] + public void Builder_BuildString_Concat() + { + var container = new ArgumentsContainer(); + container.Add(new OutputArgument("output.mp4")); + + container.Add(new ConcatArgument(concatFiles)); + + var str = builder.BuildArguments(container); + + Assert.IsTrue(str == "-i \"concat:1.mp4|2.mp4|3.mp4|4.mp4\" \"output.mp4\""); + } + + [TestMethod] + public void Builder_BuildString_Copy_Audio() + { + var str = GetArgumentsString(new CopyArgument(Channel.Audio)); + + Assert.IsTrue(str == "-i \"input.mp4\" -c:a copy \"output.mp4\""); + } + + + [TestMethod] + public void Builder_BuildString_Copy_Video() + { + var str = GetArgumentsString(new CopyArgument(Channel.Video)); + + Assert.IsTrue(str == "-i \"input.mp4\" -c:v copy \"output.mp4\""); + } + + [TestMethod] + public void Builder_BuildString_Copy_Both() + { + var str = GetArgumentsString(new CopyArgument(Channel.Both)); + + Assert.IsTrue(str == "-i \"input.mp4\" -c copy \"output.mp4\""); + } + + [TestMethod] + public void Builder_BuildString_CpuSpeed() + { + var str = GetArgumentsString(new CpuSpeedArgument(10)); + + Assert.IsTrue(str == "-i \"input.mp4\" -quality good -cpu-used 10 -deadline realtime \"output.mp4\""); + } + + [TestMethod] + public void Builder_BuildString_ForceFormat() + { + var str = GetArgumentsString(new ForceFormatArgument(VideoCodec.LibX264)); + + Assert.IsTrue(str == "-i \"input.mp4\" -f libx264 \"output.mp4\""); + } + + [TestMethod] + public void Builder_BuildString_FrameOutputCount() + { + var str = GetArgumentsString(new FrameOutputCountArgument(50)); + + Assert.IsTrue(str == "-i \"input.mp4\" -vframes 50 \"output.mp4\""); + } + + [TestMethod] + public void Builder_BuildString_FrameRate() + { + var str = GetArgumentsString(new FrameRateArgument(50)); + + Assert.IsTrue(str == "-i \"input.mp4\" -r 50 \"output.mp4\""); + } + + [TestMethod] + public void Builder_BuildString_Loop() + { + var str = GetArgumentsString(new LoopArgument(50)); + + Assert.IsTrue(str == "-i \"input.mp4\" -loop 50 \"output.mp4\""); + } + + [TestMethod] + public void Builder_BuildString_Seek() + { + var str = GetArgumentsString(new SeekArgument(TimeSpan.FromSeconds(10))); + + Assert.IsTrue(str == "-i \"input.mp4\" -ss 00:00:10 \"output.mp4\""); + } + + [TestMethod] + public void Builder_BuildString_Shortest() + { + var str = GetArgumentsString(new ShortestArgument(true)); + + Assert.IsTrue(str == "-i \"input.mp4\" -shortest \"output.mp4\""); + } + + [TestMethod] + public void Builder_BuildString_Size() + { + var str = GetArgumentsString(new SizeArgument(1920, 1080)); + + Assert.IsTrue(str == "-i \"input.mp4\" -s 1920x1080 \"output.mp4\""); + } + + [TestMethod] + public void Builder_BuildString_Speed() + { + var str = GetArgumentsString(new SpeedArgument(Speed.Fast)); + + Assert.IsTrue(str == "-i \"input.mp4\" -preset fast \"output.mp4\""); + } + + [TestMethod] + public void Builder_BuildString_StartNumber() + { + var str = GetArgumentsString(new StartNumberArgument(50)); + + Assert.IsTrue(str == "-i \"input.mp4\" -start_number 50 \"output.mp4\""); + } + + [TestMethod] + public void Builder_BuildString_Threads_1() + { + var str = GetArgumentsString(new ThreadsArgument(50)); + + Assert.IsTrue(str == "-i \"input.mp4\" -threads 50 \"output.mp4\""); + } + + [TestMethod] + public void Builder_BuildString_Threads_2() + { + var str = GetArgumentsString(new ThreadsArgument(true)); + + Assert.IsTrue(str == $"-i \"input.mp4\" -threads {Environment.ProcessorCount} \"output.mp4\""); + } + + + [TestMethod] + public void Builder_BuildString_Codec() + { + var str = GetArgumentsString(new VideoCodecArgument(VideoCodec.LibX264)); + + Assert.IsTrue(str == "-i \"input.mp4\" -codec:v libx264 -pix_fmt yuv420p \"output.mp4\""); + } + + [TestMethod] + public void Builder_BuildString_Codec_Override() + { + var str = GetArgumentsString(new VideoCodecArgument(VideoCodec.LibX264), new OverrideArgument()); + + Assert.IsTrue(str == "-i \"input.mp4\" -codec:v libx264 -pix_fmt yuv420p \"output.mp4\" -y"); + } + } +} diff --git a/FFMpegCore.Test/AudioTest.cs b/FFMpegCore.Test/AudioTest.cs new file mode 100644 index 0000000..20f1d89 --- /dev/null +++ b/FFMpegCore.Test/AudioTest.cs @@ -0,0 +1,84 @@ +using FFMpegCore.Enums; +using FFMpegCore.Tests.Resources; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.IO; + +namespace FFMpegCore.Tests +{ + [TestClass] + public class AudioTest : BaseTest + { + [TestMethod] + public void Audio_Remove() + { + var output = Input.OutputLocation(VideoType.Mp4); + + try + { + Encoder.Mute(VideoInfo.FromFileInfo(Input), output); + + Assert.IsTrue(File.Exists(output.FullName)); + } + finally + { + if (File.Exists(output.FullName)) + output.Delete(); + } + } + + [TestMethod] + public void Audio_Save() + { + var output = Input.OutputLocation(AudioType.Mp3); + + try + { + Encoder.ExtractAudio(VideoInfo.FromFileInfo(Input), output); + + Assert.IsTrue(File.Exists(output.FullName)); + } + finally + { + if (File.Exists(output.FullName)) + output.Delete(); + } + } + + [TestMethod] + public void Audio_Add() + { + var output = Input.OutputLocation(VideoType.Mp4); + try + { + var input = VideoInfo.FromFileInfo(VideoLibrary.LocalVideoNoAudio); + Encoder.ReplaceAudio(input, VideoLibrary.LocalAudio, output); + + Assert.AreEqual(input.Duration, VideoInfo.FromFileInfo(output).Duration); + Assert.IsTrue(File.Exists(output.FullName)); + } + finally + { + if (File.Exists(output.FullName)) + output.Delete(); + } + } + + [TestMethod] + public void Image_AddAudio() + { + var output = Input.OutputLocation(VideoType.Mp4); + + try + { + var result = Encoder.PosterWithAudio(new FileInfo(VideoLibrary.LocalCover.FullName), VideoLibrary.LocalAudio, output); + Assert.IsTrue(result.Duration.TotalSeconds > 0); + Assert.IsTrue(result.Exists); + } + finally + { + if (File.Exists(output.FullName)) + output.Delete(); + } + } + } +} \ No newline at end of file diff --git a/FFMpegCore.Test/BaseTest.cs b/FFMpegCore.Test/BaseTest.cs new file mode 100644 index 0000000..e564ee0 --- /dev/null +++ b/FFMpegCore.Test/BaseTest.cs @@ -0,0 +1,19 @@ +using System.Configuration; +using System.IO; +using FFMpegCore.FFMPEG; +using FFMpegCore.Tests.Resources; + +namespace FFMpegCore.Tests +{ + public class BaseTest + { + protected FFMpeg Encoder; + protected FileInfo Input; + + public BaseTest() + { + Encoder = new FFMpeg(); + Input = VideoLibrary.LocalVideo; + } + } +} \ No newline at end of file diff --git a/FFMpegCore.Test/FFMpegCore.Test.csproj b/FFMpegCore.Test/FFMpegCore.Test.csproj new file mode 100644 index 0000000..b3defdd --- /dev/null +++ b/FFMpegCore.Test/FFMpegCore.Test.csproj @@ -0,0 +1,56 @@ + + + + netcoreapp2.1 + + false + + + + + + + + + + + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + + + + + + diff --git a/FFMpegCore.Test/Resources/VideoLibrary.cs b/FFMpegCore.Test/Resources/VideoLibrary.cs new file mode 100644 index 0000000..b4f6c53 --- /dev/null +++ b/FFMpegCore.Test/Resources/VideoLibrary.cs @@ -0,0 +1,49 @@ +using System; +using System.IO; +using FFMpegCore.Enums; + +namespace FFMpegCore.Tests.Resources +{ + public enum AudioType + { + Mp3 + } + + public enum ImageType + { + Png + } + + public static class VideoLibrary + { + public static readonly FileInfo LocalVideo = new FileInfo(".\\Resources\\input.mp4"); + public static readonly FileInfo LocalVideoNoAudio = new FileInfo(".\\Resources\\mute.mp4"); + public static readonly FileInfo LocalAudio = new FileInfo(".\\Resources\\audio.mp3"); + public static readonly FileInfo LocalCover = new FileInfo(".\\Resources\\cover.png"); + public static readonly FileInfo ImageDirectory = new FileInfo(".\\Resources\\images"); + public static readonly FileInfo ImageJoinOutput = new FileInfo(".\\Resources\\images\\output.mp4"); + + public static FileInfo OutputLocation(this FileInfo file, VideoType type) + { + return OutputLocation(file, type, "_converted"); + } + + public static FileInfo OutputLocation(this FileInfo file, AudioType type) + { + return OutputLocation(file, type, "_audio"); + } + + public static FileInfo OutputLocation(this FileInfo file, ImageType type) + { + return OutputLocation(file, type, "_screenshot"); + } + + public static FileInfo OutputLocation(this FileInfo file, Enum type, string keyword) + { + string originalLocation = file.Directory.FullName, + outputFile = file.Name.Replace(file.Extension, keyword + "." + type.ToString().ToLower()); + + return new FileInfo($"{originalLocation}\\{outputFile}"); + } + } +} \ No newline at end of file diff --git a/FFMpegCore.Test/Resources/audio.mp3 b/FFMpegCore.Test/Resources/audio.mp3 new file mode 100644 index 0000000..1c32364 Binary files /dev/null and b/FFMpegCore.Test/Resources/audio.mp3 differ diff --git a/FFMpegCore.Test/Resources/cover.png b/FFMpegCore.Test/Resources/cover.png new file mode 100644 index 0000000..71426c8 Binary files /dev/null and b/FFMpegCore.Test/Resources/cover.png differ diff --git a/FFMpegCore.Test/Resources/images/a.png b/FFMpegCore.Test/Resources/images/a.png new file mode 100644 index 0000000..5c8a18c Binary files /dev/null and b/FFMpegCore.Test/Resources/images/a.png differ diff --git a/FFMpegCore.Test/Resources/images/b.png b/FFMpegCore.Test/Resources/images/b.png new file mode 100644 index 0000000..159f7eb Binary files /dev/null and b/FFMpegCore.Test/Resources/images/b.png differ diff --git a/FFMpegCore.Test/Resources/images/c.png b/FFMpegCore.Test/Resources/images/c.png new file mode 100644 index 0000000..1fa3ecc Binary files /dev/null and b/FFMpegCore.Test/Resources/images/c.png differ diff --git a/FFMpegCore.Test/Resources/images/d.png b/FFMpegCore.Test/Resources/images/d.png new file mode 100644 index 0000000..15d316a Binary files /dev/null and b/FFMpegCore.Test/Resources/images/d.png differ diff --git a/FFMpegCore.Test/Resources/images/e.png b/FFMpegCore.Test/Resources/images/e.png new file mode 100644 index 0000000..205cd4b Binary files /dev/null and b/FFMpegCore.Test/Resources/images/e.png differ diff --git a/FFMpegCore.Test/Resources/images/f.png b/FFMpegCore.Test/Resources/images/f.png new file mode 100644 index 0000000..5c845d6 Binary files /dev/null and b/FFMpegCore.Test/Resources/images/f.png differ diff --git a/FFMpegCore.Test/Resources/input.mp4 b/FFMpegCore.Test/Resources/input.mp4 new file mode 100644 index 0000000..73bbd71 Binary files /dev/null and b/FFMpegCore.Test/Resources/input.mp4 differ diff --git a/FFMpegCore.Test/Resources/mute.mp4 b/FFMpegCore.Test/Resources/mute.mp4 new file mode 100644 index 0000000..095e8ba Binary files /dev/null and b/FFMpegCore.Test/Resources/mute.mp4 differ diff --git a/FFMpegCore.Test/VideoTest.cs b/FFMpegCore.Test/VideoTest.cs new file mode 100644 index 0000000..72b27a5 --- /dev/null +++ b/FFMpegCore.Test/VideoTest.cs @@ -0,0 +1,282 @@ +using FFMpegCore.Enums; +using FFMpegCore.FFMPEG; +using FFMpegCore.FFMPEG.Arguments; +using FFMpegCore.FFMPEG.Enums; +using FFMpegCore.Tests.Resources; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Collections.Generic; +using System.Drawing.Imaging; +using System.IO; +using System.Linq; + +namespace FFMpegCore.Tests +{ + [TestClass] + public class VideoTest : BaseTest + { + public bool Convert(VideoType type, bool multithreaded = false, VideoSize size = VideoSize.Original) + { + var output = Input.OutputLocation(type); + + try + { + var input = VideoInfo.FromFileInfo(Input); + + Encoder.Convert(input, output, type, size: size, multithreaded: multithreaded); + + var outputVideo = new VideoInfo(output.FullName); + + Assert.IsTrue(File.Exists(output.FullName)); + Assert.AreEqual(outputVideo.Duration, input.Duration); + if (size == VideoSize.Original) + { + Assert.AreEqual(outputVideo.Width, input.Width); + Assert.AreEqual(outputVideo.Height, input.Height); + } else + { + Assert.AreNotEqual(outputVideo.Width, input.Width); + Assert.AreNotEqual(outputVideo.Height, input.Height); + Assert.AreEqual(outputVideo.Height, (int)size); + } + return File.Exists(output.FullName) && + outputVideo.Duration == input.Duration && + ( + ( + size == VideoSize.Original && + outputVideo.Width == input.Width && + outputVideo.Height == input.Height + ) || + ( + size != VideoSize.Original && + outputVideo.Width != input.Width && + outputVideo.Height != input.Height && + outputVideo.Height == (int)size + ) + ); + } + finally + { + if (File.Exists(output.FullName)) + File.Delete(output.FullName); + } + } + + public void Convert(VideoType type, ArgumentsContainer container) + { + var output = Input.OutputLocation(type); + + try + { + var input = VideoInfo.FromFileInfo(Input); + + container.Add(new InputArgument(input)); + container.Add(new OutputArgument(output)); + var scaling = container.Find(); + + Encoder.Convert(container); + + var outputVideo = new VideoInfo(output.FullName); + + Assert.IsTrue(File.Exists(output.FullName)); + Assert.AreEqual(outputVideo.Duration, input.Duration); + + if (scaling == null) + { + Assert.AreEqual(outputVideo.Width, input.Width); + Assert.AreEqual(outputVideo.Height, input.Height); + } else + { + if (scaling.Value.Width != -1) + { + Assert.AreEqual(outputVideo.Width, scaling.Value.Width); + } + + if (scaling.Value.Height != -1) + { + Assert.AreEqual(outputVideo.Height, scaling.Value.Height); + } + + Assert.AreNotEqual(outputVideo.Width, input.Width); + Assert.AreNotEqual(outputVideo.Height, input.Height); + } + } + finally + { + if (File.Exists(output.FullName)) + File.Delete(output.FullName); + } + } + + [TestMethod] + public void Video_ToMP4() + { + Convert(VideoType.Mp4); + } + + [TestMethod] + public void Video_ToMP4_Args() + { + var container = new ArgumentsContainer(); + container.Add(new VideoCodecArgument(VideoCodec.LibX264)); + Convert(VideoType.Mp4, container); + } + + [TestMethod] + public void Video_ToTS() + { + Convert(VideoType.Ts); + } + + [TestMethod] + public void Video_ToTS_Args() + { + var container = new ArgumentsContainer(); + container.Add(new CopyArgument()); + container.Add(new BitStreamFilterArgument(Channel.Video, Filter.H264_Mp4ToAnnexB)); + container.Add(new ForceFormatArgument(VideoCodec.MpegTs)); + Convert(VideoType.Ts, container); + } + + + [TestMethod] + public void Video_ToOGV_Resize() + { + Convert(VideoType.Ogv, true, VideoSize.Ed); + } + + [TestMethod] + public void Video_ToOGV_Resize_Args() + { + var container = new ArgumentsContainer(); + container.Add(new ScaleArgument(VideoSize.Ed)); + container.Add(new VideoCodecArgument(VideoCodec.LibTheora)); + Convert(VideoType.Ogv, container); + } + + [TestMethod] + public void Video_ToMP4_Resize() + { + Convert(VideoType.Mp4, true, VideoSize.Ed); + } + + [TestMethod] + public void Video_ToMP4_Resize_Args() + { + var container = new ArgumentsContainer(); + container.Add(new ScaleArgument(VideoSize.Ld)); + container.Add(new VideoCodecArgument(VideoCodec.LibX264)); + Convert(VideoType.Mp4, container); + } + + [TestMethod] + public void Video_ToOGV() + { + Convert(VideoType.Ogv); + } + + [TestMethod] + public void Video_ToMP4_MultiThread() + { + Convert(VideoType.Mp4, true); + } + + [TestMethod] + public void Video_ToTS_MultiThread() + { + Convert(VideoType.Ts, true); + } + + [TestMethod] + public void Video_ToOGV_MultiThread() + { + Convert(VideoType.Ogv, true); + } + + [TestMethod] + public void Video_Snapshot() + { + var output = Input.OutputLocation(ImageType.Png); + + try + { + var input = VideoInfo.FromFileInfo(Input); + + using (var bitmap = Encoder.Snapshot(input, output)) + { + Assert.AreEqual(input.Width, bitmap.Width); + Assert.AreEqual(input.Height, bitmap.Height); + Assert.AreEqual(bitmap.RawFormat, ImageFormat.Png); + } + } + finally + { + if (File.Exists(output.FullName)) + File.Delete(output.FullName); + } + } + + [TestMethod] + public void Video_Join() + { + var output = Input.OutputLocation(VideoType.Mp4); + var newInput = Input.OutputLocation(VideoType.Mp4, "duplicate"); + try + { + var input = VideoInfo.FromFileInfo(Input); + File.Copy(input.FullName, newInput.FullName); + var input2 = VideoInfo.FromFileInfo(newInput); + + var result = Encoder.Join(output, input, input2); + + Assert.IsTrue(File.Exists(output.FullName)); + Assert.AreEqual(input.Duration.TotalSeconds * 2, result.Duration.TotalSeconds); + Assert.AreEqual(input.Height, result.Height); + Assert.AreEqual(input.Width, result.Width); + } + finally + { + if (File.Exists(output.FullName)) + File.Delete(output.FullName); + + if (File.Exists(newInput.FullName)) + File.Delete(newInput.FullName); + } + } + + [TestMethod] + public void Video_Join_Image_Sequence() + { + try + { + var imageSet = new List(); + Directory.EnumerateFiles(VideoLibrary.ImageDirectory.FullName) + .Where(file => file.ToLower().EndsWith(".png")) + .ToList() + .ForEach(file => + { + for (int i = 0; i < 15; i++) + { + imageSet.Add(new ImageInfo(file)); + } + }); + + var result = Encoder.JoinImageSequence(VideoLibrary.ImageJoinOutput, images: imageSet.ToArray()); + + VideoLibrary.ImageJoinOutput.Refresh(); + + Assert.IsTrue(VideoLibrary.ImageJoinOutput.Exists); + Assert.AreEqual(3, result.Duration.Seconds); + Assert.AreEqual(imageSet.First().Width, result.Width); + Assert.AreEqual(imageSet.First().Height, result.Height); + } + finally + { + VideoLibrary.ImageJoinOutput.Refresh(); + if (VideoLibrary.ImageJoinOutput.Exists) + { + VideoLibrary.ImageJoinOutput.Delete(); + } + } + } + } +} diff --git a/FFMpegCore.sln b/FFMpegCore.sln new file mode 100644 index 0000000..eab20fd --- /dev/null +++ b/FFMpegCore.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28307.329 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FFMpegCore", "FFMpegCore\FFMpegCore.csproj", "{19DE2EC2-9955-4712-8096-C22EF6713E4F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMpegCore.Test", "FFMpegCore.Test\FFMpegCore.Test.csproj", "{F20C8353-72D9-454B-9F16-3624DBAD2328}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {19DE2EC2-9955-4712-8096-C22EF6713E4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {19DE2EC2-9955-4712-8096-C22EF6713E4F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {19DE2EC2-9955-4712-8096-C22EF6713E4F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {19DE2EC2-9955-4712-8096-C22EF6713E4F}.Release|Any CPU.Build.0 = Release|Any CPU + {F20C8353-72D9-454B-9F16-3624DBAD2328}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F20C8353-72D9-454B-9F16-3624DBAD2328}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F20C8353-72D9-454B-9F16-3624DBAD2328}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F20C8353-72D9-454B-9F16-3624DBAD2328}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F1B53337-60E7-49CB-A171-D4AEF6B4D5F0} + EndGlobalSection +EndGlobal diff --git a/FFMpegCore/Enums/FileExtension.cs b/FFMpegCore/Enums/FileExtension.cs new file mode 100644 index 0000000..212e3db --- /dev/null +++ b/FFMpegCore/Enums/FileExtension.cs @@ -0,0 +1,38 @@ +using FFMpegCore.FFMPEG.Enums; +using System; + +namespace FFMpegCore.Enums +{ + public static class FileExtension + { + public static string ForType(VideoType type) + { + switch (type) + { + case VideoType.Mp4: return Mp4; + case VideoType.Ogv: return Ogv; + case VideoType.Ts: return Ts; + case VideoType.WebM: return WebM; + default: throw new Exception("The extension for this video type is not defined."); + } + } + public static string ForCodec(VideoCodec type) + { + switch (type) + { + case VideoCodec.LibX264: return Mp4; + case VideoCodec.LibVpx: return WebM; + case VideoCodec.LibTheora: return Ogv; + case VideoCodec.MpegTs: return Ts; + case VideoCodec.Png: return Png; + default: throw new Exception("The extension for this video type is not defined."); + } + } + public static readonly string Mp4 = ".mp4"; + public static readonly string Mp3 = ".mp3"; + public static readonly string Ts = ".ts"; + public static readonly string Ogv = ".ogv"; + public static readonly string Png = ".png"; + public static readonly string WebM = ".webm"; + } +} diff --git a/FFMpegCore/Enums/VideoType.cs b/FFMpegCore/Enums/VideoType.cs new file mode 100644 index 0000000..582330d --- /dev/null +++ b/FFMpegCore/Enums/VideoType.cs @@ -0,0 +1,10 @@ +namespace FFMpegCore.Enums +{ + public enum VideoType + { + Mp4, + Ogv, + Ts, + WebM + } +} \ No newline at end of file diff --git a/FFMpegCore/Extend/BitmapExtensions.cs b/FFMpegCore/Extend/BitmapExtensions.cs new file mode 100644 index 0000000..050ada5 --- /dev/null +++ b/FFMpegCore/Extend/BitmapExtensions.cs @@ -0,0 +1,31 @@ +using System; +using System.Drawing; +using System.IO; +using FFMpegCore.FFMPEG; + +namespace FFMpegCore.Extend +{ + public static class BitmapExtensions + { + public static VideoInfo AddAudio(this Bitmap poster, FileInfo audio, FileInfo output) + { + var destination = $"{Environment.TickCount}.png"; + + poster.Save(destination); + + var tempFile = new FileInfo(destination); + try + { + return new FFMpeg().PosterWithAudio(tempFile, audio, output); + } + catch(Exception e) + { + throw; + } + finally + { + tempFile.Delete(); + } + } + } +} \ No newline at end of file diff --git a/FFMpegCore/Extend/UriExtensions.cs b/FFMpegCore/Extend/UriExtensions.cs new file mode 100644 index 0000000..0d7629e --- /dev/null +++ b/FFMpegCore/Extend/UriExtensions.cs @@ -0,0 +1,14 @@ +using System; +using System.IO; +using FFMpegCore.FFMPEG; + +namespace FFMpegCore.Extend +{ + public static class UriExtensions + { + public static VideoInfo SaveStream(this Uri uri, FileInfo output) + { + return new FFMpeg().SaveM3U8Stream(uri, output); + } + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMPEG/Arguments/Argument.cs b/FFMpegCore/FFMPEG/Arguments/Argument.cs new file mode 100644 index 0000000..5340e7b --- /dev/null +++ b/FFMpegCore/FFMPEG/Arguments/Argument.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FFMpegCore.FFMPEG.Arguments +{ + /// + /// Abstract class implements basic functionality of ffmpeg arguments + /// + public abstract class Argument + { + /// + /// String representation of the argument + /// + /// String representation of the argument + public abstract string GetStringValue(); + + public override string ToString() + { + return GetStringValue(); + } + } + + /// + /// Abstract class implements basic functionality of ffmpeg arguments with one value property + /// + public abstract class Argument : Argument + { + private T _value; + + /// + /// Value type of + /// + public T Value { get => _value; set { CheckValue(value); _value = value; } } + + public Argument() { } + + public Argument(T value) + { + Value = value; + } + + protected virtual void CheckValue(T value) + { + + } + } + + /// + /// Abstract class implements basic functionality of ffmpeg arguments with two values properties + /// + public abstract class Argument : Argument + { + + private T1 _first; + private T2 _second; + + /// + /// First value type of + /// + public T1 First { get => _first; set { CheckFirst(_first); _first = value; } } + + /// + /// Second value type of + /// + public T2 Second { get => _second; set { CheckSecond(_second); _second = value; } } + + public Argument() { } + + public Argument(T1 first, T2 second) + { + First = first; + Second = second; + } + + protected virtual void CheckFirst(T1 value) + { + + } + + protected virtual void CheckSecond(T2 value) + { + + } + } +} diff --git a/FFMpegCore/FFMPEG/Arguments/ArgumentsContainer.cs b/FFMpegCore/FFMPEG/Arguments/ArgumentsContainer.cs new file mode 100644 index 0000000..b71cda4 --- /dev/null +++ b/FFMpegCore/FFMPEG/Arguments/ArgumentsContainer.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FFMpegCore.FFMPEG.Arguments +{ + /// + /// Container of arguments represented parameters of FFMPEG process + /// + public class ArgumentsContainer : IDictionary + { + Dictionary _args; + + public ArgumentsContainer() + { + _args = new Dictionary(); + } + + public Argument this[Type key] { get => ((IDictionary)_args)[key]; set => ((IDictionary)_args)[key] = value; } + + public ICollection Keys => ((IDictionary)_args).Keys; + + public ICollection Values => ((IDictionary)_args).Values; + + public int Count => ((IDictionary)_args).Count; + + public bool IsReadOnly => ((IDictionary)_args).IsReadOnly; + + /// + /// This method is not supported, left for interface support + /// + /// + /// + [Obsolete] + public void Add(Type key, Argument value) + { + throw new InvalidOperationException("Not supported operation"); + } + + /// + /// This method is not supported, left for interface support + /// + /// + /// + [Obsolete] + public void Add(KeyValuePair item) + { + throw new InvalidOperationException("Not supported operation"); + } + + /// + /// Clears collection of arguments + /// + public void Clear() + { + ((IDictionary)_args).Clear(); + } + + /// + /// Returns if contains item + /// + /// Searching item + /// Returns if contains item + public bool Contains(KeyValuePair item) + { + return ((IDictionary)_args).Contains(item); + } + + /// + /// Adds argument to collection + /// + /// Argument that should be added to collection + public void Add(Argument value) + { + ((IDictionary)_args).Add(value.GetType(), value); + } + + /// + /// Checks if container contains output and input parameters + /// + /// + public bool ContainsInputOutput() + { + return ((ContainsKey(typeof(InputArgument)) && !ContainsKey(typeof(ConcatArgument))) || + (!ContainsKey(typeof(InputArgument)) && ContainsKey(typeof(ConcatArgument)))) + && ContainsKey(typeof(OutputArgument)); + } + + /// + /// Checks if contains argument of type + /// + /// Type of argument is seraching + /// + public bool ContainsKey(Type key) + { + return ((IDictionary)_args).ContainsKey(key); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + ((IDictionary)_args).CopyTo(array, arrayIndex); + } + + public IEnumerator> GetEnumerator() + { + return ((IDictionary)_args).GetEnumerator(); + } + + public bool Remove(Type key) + { + return ((IDictionary)_args).Remove(key); + } + + public bool Remove(KeyValuePair item) + { + return ((IDictionary)_args).Remove(item); + } + + public bool TryGetValue(Type key, out Argument value) + { + return ((IDictionary)_args).TryGetValue(key, out value); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IDictionary)_args).GetEnumerator(); + } + + /// + /// Shortcut for finding arguments inside collection + /// + /// Type of argument + /// + public T Find() where T : Argument + { + if (ContainsKey(typeof(T))) + return (T)_args[typeof(T)]; + return null; + } + /// + /// Shortcut for checking if contains arguments inside collection + /// + /// Type of argument + /// + public bool Contains() where T : Argument + { + if (ContainsKey(typeof(T))) + return true; + return false; + } + } +} diff --git a/FFMpegCore/FFMPEG/Arguments/ArgumentsStringifier.cs b/FFMpegCore/FFMPEG/Arguments/ArgumentsStringifier.cs new file mode 100644 index 0000000..ca6d231 --- /dev/null +++ b/FFMpegCore/FFMPEG/Arguments/ArgumentsStringifier.cs @@ -0,0 +1,205 @@ +using FFMpegCore.FFMPEG.Enums; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; + +namespace FFMpegCore.FFMPEG.Arguments +{ + internal static class ArgumentsStringifier + { + internal static string Speed(Speed speed) + { + return $"-preset {speed.ToString().ToLower()} "; + } + + internal static string Speed(int cpu) + { + return $"-quality good -cpu-used {cpu} -deadline realtime "; + } + + internal static string Audio(AudioCodec codec, AudioQuality bitrate) + { + return Audio(codec) + Audio(bitrate); + } + + internal static string Audio(AudioCodec codec, int bitrate) + { + return Audio(codec) + Audio(bitrate); + } + + internal static string Audio(AudioCodec codec) + { + return $"-codec:a {codec.ToString().ToLower()} "; + } + + internal static string Audio(AudioQuality bitrate) + { + return Audio((int)bitrate); + } + + internal static string Audio(int bitrate) + { + return $"-b:a {bitrate}k -strict experimental "; + } + + internal static string Video(VideoCodec codec, int bitrate = 0) + { + var video = $"-codec:v {codec.ToString().ToLower()} -pix_fmt yuv420p "; + + if (bitrate > 0) + { + video += $"-b:v {bitrate}k "; + } + + return video; + } + + internal static string Threads(bool multiThread) + { + var threadCount = multiThread + ? Environment.ProcessorCount + : 1; + + return Threads(threadCount); + } + + internal static string Threads(int threads) + { + return $"-threads {threads} "; + } + + internal static string Input(Uri uri) + { + return Input(uri.AbsolutePath); + } + + internal static string Disable(Channel type) + { + switch (type) + { + case Channel.Video: + return "-vn "; + case Channel.Audio: + return "-an "; + default: + return string.Empty; + } + } + + internal static string Input(VideoInfo input) + { + return $"-i \"{input.FullName}\" "; + } + + internal static string Input(FileInfo input) + { + return $"-i \"{input.FullName}\" "; + } + + internal static string Output(FileInfo output) + { + return $"\"{output.FullName}\""; + } + + internal static string Output(string output) + { + return $"\"{output}\""; + } + + internal static string Input(string template) + { + return $"-i \"{template}\" "; + } + + internal static string Scale(VideoSize size, int width =-1) + { + return size == VideoSize.Original ? string.Empty : Scale(width, (int)size); + } + + internal static string Scale(int width, int height) + { + return $"-vf scale={width}:{height} "; + } + + internal static string Scale(Size size) + { + return Scale(size.Width, size.Height); + } + + internal static string Size(Size? size) + { + if (!size.HasValue) return string.Empty; + + var formatedSize = $"{size.Value.Width}x{size.Value.Height}"; + + return $"-s {formatedSize} "; + } + + internal static string ForceFormat(VideoCodec codec) + { + return $"-f {codec.ToString().ToLower()} "; + } + + internal static string BitStreamFilter(Channel type, Filter filter) + { + switch (type) + { + case Channel.Audio: + return $"-bsf:a {filter.ToString().ToLower()} "; + case Channel.Video: + return $"-bsf:v {filter.ToString().ToLower()} "; + default: + return string.Empty; + } + } + + internal static string Copy(Channel type = Channel.Both) + { + switch (type) + { + case Channel.Audio: + return "-c:a copy "; + case Channel.Video: + return "-c:v copy "; + default: + return "-c copy "; + } + } + + internal static string Seek(TimeSpan? seek) + { + return !seek.HasValue ? string.Empty : $"-ss {seek} "; + } + + internal static string FrameOutputCount(int number) + { + return $"-vframes {number} "; + } + + internal static string Loop(int count) + { + return $"-loop {count} "; + } + + internal static string FinalizeAtShortestInput(bool applicable) + { + return applicable ? "-shortest " : string.Empty; + } + + internal static string InputConcat(IEnumerable paths) + { + return $"-i \"concat:{string.Join(@"|", paths)}\" "; + } + + internal static string FrameRate(double frameRate) + { + return $"-r {frameRate} "; + } + + internal static string StartNumber(int v) + { + return $"-start_number {v} "; + } + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMPEG/Arguments/AudioCodecArgument.cs b/FFMpegCore/FFMPEG/Arguments/AudioCodecArgument.cs new file mode 100644 index 0000000..73e396f --- /dev/null +++ b/FFMpegCore/FFMPEG/Arguments/AudioCodecArgument.cs @@ -0,0 +1,47 @@ +using FFMpegCore.FFMPEG.Enums; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FFMpegCore.FFMPEG.Arguments +{ + /// + /// Represents parameter of audio codec and it's quality + /// + public class AudioCodecArgument : Argument + { + /// + /// Bitrate of audio channel + /// + public int Bitrate { get; protected set; } = (int)AudioQuality.Normal; + + public AudioCodecArgument() + { + } + + public AudioCodecArgument(AudioCodec value) : base(value) + { + } + + public AudioCodecArgument(AudioCodec value, AudioQuality bitrate) : base(value) + { + Bitrate = (int)bitrate; + } + + public AudioCodecArgument(AudioCodec value, int bitrate) : base(value) + { + Bitrate = bitrate; + } + + /// + /// String representation of the argument + /// + /// String representation of the argument + public override string GetStringValue() + { + return ArgumentsStringifier.Audio(Value, Bitrate); + } + } +} diff --git a/FFMpegCore/FFMPEG/Arguments/BitStreamFilterArgument.cs b/FFMpegCore/FFMPEG/Arguments/BitStreamFilterArgument.cs new file mode 100644 index 0000000..a986fa8 --- /dev/null +++ b/FFMpegCore/FFMPEG/Arguments/BitStreamFilterArgument.cs @@ -0,0 +1,32 @@ +using FFMpegCore.FFMPEG.Enums; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FFMpegCore.FFMPEG.Arguments +{ + /// + /// Represents parameter of bitstream filter + /// + public class BitStreamFilterArgument : Argument + { + public BitStreamFilterArgument() + { + } + + public BitStreamFilterArgument(Channel first, Filter second) : base(first, second) + { + } + + /// + /// String representation of the argument + /// + /// String representation of the argument + public override string GetStringValue() + { + return ArgumentsStringifier.BitStreamFilter(First, Second); + } + } +} diff --git a/FFMpegCore/FFMPEG/Arguments/ConcatArgument.cs b/FFMpegCore/FFMPEG/Arguments/ConcatArgument.cs new file mode 100644 index 0000000..76eea22 --- /dev/null +++ b/FFMpegCore/FFMPEG/Arguments/ConcatArgument.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FFMpegCore.FFMPEG.Arguments +{ + + /// + /// Represents parameter of concat argument + /// Used for creating video from multiple images or videos + /// + public class ConcatArgument : Argument>, IEnumerable + { + public ConcatArgument() + { + Value = new List(); + } + + public ConcatArgument(IEnumerable value) : base(value) + { + } + + public IEnumerator GetEnumerator() + { + return Value.GetEnumerator(); + } + + /// + /// String representation of the argument + /// + /// String representation of the argument + public override string GetStringValue() + { + return ArgumentsStringifier.InputConcat(Value); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/FFMpegCore/FFMPEG/Arguments/CopyArgument.cs b/FFMpegCore/FFMPEG/Arguments/CopyArgument.cs new file mode 100644 index 0000000..2710b58 --- /dev/null +++ b/FFMpegCore/FFMPEG/Arguments/CopyArgument.cs @@ -0,0 +1,34 @@ +using FFMpegCore.FFMPEG.Enums; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FFMpegCore.FFMPEG.Arguments +{ + /// + /// Represents parameter of copy parameter + /// Defines if channel (audio, video or both) should be copied to output file + /// + public class CopyArgument : Argument + { + public CopyArgument() + { + Value = Channel.Both; + } + + public CopyArgument(Channel value = Channel.Both) : base(value) + { + } + + /// + /// String representation of the argument + /// + /// String representation of the argument + public override string GetStringValue() + { + return ArgumentsStringifier.Copy(Value); + } + } +} diff --git a/FFMpegCore/FFMPEG/Arguments/CpuSpeedArgument.cs b/FFMpegCore/FFMPEG/Arguments/CpuSpeedArgument.cs new file mode 100644 index 0000000..16a9dc5 --- /dev/null +++ b/FFMpegCore/FFMPEG/Arguments/CpuSpeedArgument.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FFMpegCore.FFMPEG.Arguments +{ + /// + /// Represents cpu speed parameter + /// + public class CpuSpeedArgument : Argument + { + public CpuSpeedArgument() + { + } + + public CpuSpeedArgument(int value) : base(value) + { + } + + /// + /// String representation of the argument + /// + /// String representation of the argument + public override string GetStringValue() + { + return ArgumentsStringifier.Speed(Value); + } + } +} diff --git a/FFMpegCore/FFMPEG/Arguments/FFArgumentBuilder.cs b/FFMpegCore/FFMPEG/Arguments/FFArgumentBuilder.cs new file mode 100644 index 0000000..bc8c81a --- /dev/null +++ b/FFMpegCore/FFMPEG/Arguments/FFArgumentBuilder.cs @@ -0,0 +1,122 @@ +using FFMpegCore.Enums; +using FFMpegCore.Helpers; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FFMpegCore.FFMPEG.Arguments +{ + /// + /// Builds parameters string from that would be passed to ffmpeg process + /// + public class FFArgumentBuilder : IArgumentBuilder + { + /// + /// Builds parameters string from that would be passed to ffmpeg process + /// + /// Container of arguments + /// Parameters string + public string BuildArguments(ArgumentsContainer container) + { + if (!container.ContainsInputOutput()) + throw new ArgumentException("No input or output parameter found", nameof(container)); + + CheckContainerException(container); + + StringBuilder sb = new StringBuilder(); + + sb.Append(GetInput(container).GetStringValue().Trim() + " "); + + foreach(var a in container) + { + if(!IsInputOrOutput(a.Key)) + { + sb.Append(a.Value.GetStringValue().Trim() + " "); + } + } + + sb.Append(container[typeof(OutputArgument)].GetStringValue().Trim()); + + var overrideArg = container.Find(); + if (overrideArg != null) + sb.Append(" " + overrideArg.GetStringValue().Trim()); + + return sb.ToString(); + } + + /// + /// Builds parameters string from that would be passed to ffmpeg process + /// + /// Container of arguments + /// Input file + /// Output file + /// Parameters string + public string BuildArguments(ArgumentsContainer container, FileInfo input, FileInfo output) + { + CheckContainerException(container); + CheckExtensionOfOutputExtension(container, output); + FFMpegHelper.ConversionExceptionCheck(input, output); + + + StringBuilder sb = new StringBuilder(); + + var inputA = new InputArgument(input); + var outputA = new OutputArgument(); + + sb.Append(inputA.GetStringValue().Trim() + " "); + + foreach (var a in container) + { + if (!IsInputOrOutput(a.Key)) + { + sb.Append(a.Value.GetStringValue().Trim() + " "); + } + } + + sb.Append(outputA.GetStringValue().Trim()); + + var overrideArg = container.Find(); + if (overrideArg != null) + sb.Append(" " + overrideArg.GetStringValue().Trim()); + + return sb.ToString(); + } + + private void CheckContainerException(ArgumentsContainer container) + { + //TODO: implement arguments check + } + + private void CheckExtensionOfOutputExtension(ArgumentsContainer container, FileInfo output) + { + if(container.ContainsKey(typeof(VideoCodecArgument))) + { + var codec = (VideoCodecArgument)container[typeof(VideoCodecArgument)]; + FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.ForCodec(codec.Value)); + } + } + + private Argument GetInput(ArgumentsContainer container) + { + if (container.ContainsKey(typeof(InputArgument))) + return container[typeof(InputArgument)]; + else if (container.ContainsKey(typeof(ConcatArgument))) + return container[typeof(ConcatArgument)]; + else + throw new ArgumentException("No inputs found"); + } + + private bool IsInputOrOutput(Argument arg) + { + return IsInputOrOutput(arg.GetType()); + } + + private bool IsInputOrOutput(Type arg) + { + return (arg == typeof(InputArgument)) || (arg == typeof(ConcatArgument)) || (arg == typeof(OutputArgument)) || (arg == typeof(OverrideArgument)); + } + } +} diff --git a/FFMpegCore/FFMPEG/Arguments/ForceFormatArgument.cs b/FFMpegCore/FFMPEG/Arguments/ForceFormatArgument.cs new file mode 100644 index 0000000..5debf8f --- /dev/null +++ b/FFMpegCore/FFMPEG/Arguments/ForceFormatArgument.cs @@ -0,0 +1,32 @@ +using FFMpegCore.FFMPEG.Enums; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FFMpegCore.FFMPEG.Arguments +{ + /// + /// Represents force format parameter + /// + public class ForceFormatArgument : Argument + { + public ForceFormatArgument() + { + } + + public ForceFormatArgument(VideoCodec value) : base(value) + { + } + + /// + /// String representation of the argument + /// + /// String representation of the argument + public override string GetStringValue() + { + return ArgumentsStringifier.ForceFormat(Value); + } + } +} diff --git a/FFMpegCore/FFMPEG/Arguments/FrameOutputCountArgument.cs b/FFMpegCore/FFMPEG/Arguments/FrameOutputCountArgument.cs new file mode 100644 index 0000000..fbc5205 --- /dev/null +++ b/FFMpegCore/FFMPEG/Arguments/FrameOutputCountArgument.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FFMpegCore.FFMPEG.Arguments +{ + /// + /// Represents frame output count parameter + /// + public class FrameOutputCountArgument : Argument + { + public FrameOutputCountArgument() + { + } + + public FrameOutputCountArgument(int value) : base(value) + { + } + + /// + /// String representation of the argument + /// + /// String representation of the argument + public override string GetStringValue() + { + return ArgumentsStringifier.FrameOutputCount(Value); + } + } +} diff --git a/FFMpegCore/FFMPEG/Arguments/FrameRateArgument.cs b/FFMpegCore/FFMPEG/Arguments/FrameRateArgument.cs new file mode 100644 index 0000000..ca8bd56 --- /dev/null +++ b/FFMpegCore/FFMPEG/Arguments/FrameRateArgument.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FFMpegCore.FFMPEG.Arguments +{ + /// + /// Represents frame rate parameter + /// + public class FrameRateArgument : Argument + { + public FrameRateArgument() + { + } + + public FrameRateArgument(double value) : base(value) + { + } + + /// + /// String representation of the argument + /// + /// String representation of the argument + public override string GetStringValue() + { + return ArgumentsStringifier.FrameRate(Value); + } + } +} diff --git a/FFMpegCore/FFMPEG/Arguments/IArgumentBuilder.cs b/FFMpegCore/FFMPEG/Arguments/IArgumentBuilder.cs new file mode 100644 index 0000000..6da8d9f --- /dev/null +++ b/FFMpegCore/FFMPEG/Arguments/IArgumentBuilder.cs @@ -0,0 +1,17 @@ +using FFMpegCore.FFMPEG.Enums; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FFMpegCore.FFMPEG.Arguments +{ + public interface IArgumentBuilder + { + string BuildArguments(ArgumentsContainer container); + + string BuildArguments(ArgumentsContainer container, FileInfo input, FileInfo output); + } +} diff --git a/FFMpegCore/FFMPEG/Arguments/InputArgument.cs b/FFMpegCore/FFMPEG/Arguments/InputArgument.cs new file mode 100644 index 0000000..7d5ac92 --- /dev/null +++ b/FFMpegCore/FFMPEG/Arguments/InputArgument.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FFMpegCore.FFMPEG.Arguments +{ + /// + /// Represents input parameter + /// + public class InputArgument : Argument + { + public InputArgument() + { + } + + public InputArgument(string value) : base(value) + { + } + + public InputArgument(VideoInfo value) : base(value.FullName) + { + } + + public InputArgument(FileInfo value) : base(value.FullName) + { + } + + public InputArgument(Uri value) : base(value.AbsolutePath) + { + } + + /// + /// String representation of the argument + /// + /// String representation of the argument + public override string GetStringValue() + { + return ArgumentsStringifier.Input(Value); + } + } +} diff --git a/FFMpegCore/FFMPEG/Arguments/LoopArgument.cs b/FFMpegCore/FFMPEG/Arguments/LoopArgument.cs new file mode 100644 index 0000000..1f1d968 --- /dev/null +++ b/FFMpegCore/FFMPEG/Arguments/LoopArgument.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FFMpegCore.FFMPEG.Arguments +{ + /// + /// Represents loop parameter + /// + public class LoopArgument : Argument + { + public LoopArgument() + { + } + + public LoopArgument(int value) : base(value) + { + } + + /// + /// String representation of the argument + /// + /// String representation of the argument + public override string GetStringValue() + { + return ArgumentsStringifier.Loop(Value); + } + } +} diff --git a/FFMpegCore/FFMPEG/Arguments/OutputArgument.cs b/FFMpegCore/FFMPEG/Arguments/OutputArgument.cs new file mode 100644 index 0000000..35a74df --- /dev/null +++ b/FFMpegCore/FFMPEG/Arguments/OutputArgument.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FFMpegCore.FFMPEG.Arguments +{ + /// + /// Represents output parameter + /// + public class OutputArgument : InputArgument + { + public OutputArgument() + { + } + + public OutputArgument(string value) : base(value) + { + } + + public OutputArgument(VideoInfo value) : base(value) + { + } + + public OutputArgument(FileInfo value) : base(value) + { + } + + public OutputArgument(Uri value) : base(value) + { + } + + /// + /// String representation of the argument + /// + /// String representation of the argument + public override string GetStringValue() + { + return ArgumentsStringifier.Output(Value); + } + + public FileInfo GetAsFileInfo() + { + return new FileInfo(Value); + } + } +} diff --git a/FFMpegCore/FFMPEG/Arguments/OverrideArgument.cs b/FFMpegCore/FFMPEG/Arguments/OverrideArgument.cs new file mode 100644 index 0000000..9df279e --- /dev/null +++ b/FFMpegCore/FFMPEG/Arguments/OverrideArgument.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FFMpegCore.FFMPEG.Arguments +{ + /// + /// Represents override parameter + /// If output file should be overrided if exists + /// + public class OverrideArgument : Argument + { + public OverrideArgument() + { + } + + /// + /// String representation of the argument + /// + /// String representation of the argument + public override string GetStringValue() + { + return "-y"; + } + } +} diff --git a/FFMpegCore/FFMPEG/Arguments/ScaleArgument.cs b/FFMpegCore/FFMPEG/Arguments/ScaleArgument.cs new file mode 100644 index 0000000..17a88d9 --- /dev/null +++ b/FFMpegCore/FFMPEG/Arguments/ScaleArgument.cs @@ -0,0 +1,42 @@ +using FFMpegCore.FFMPEG.Enums; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FFMpegCore.FFMPEG.Arguments +{ + /// + /// Represents scale parameter + /// + public class ScaleArgument : Argument + { + public ScaleArgument() + { + } + + public ScaleArgument(Size value) : base(value) + { + } + + public ScaleArgument(int width, int heignt) : base(new Size(width, heignt)) + { + } + + public ScaleArgument(VideoSize videosize) + { + Value = videosize == VideoSize.Original ? new Size(-1, -1) : new Size(-1, (int)videosize); + } + + /// + /// String representation of the argument + /// + /// String representation of the argument + public override string GetStringValue() + { + return ArgumentsStringifier.Scale(Value); + } + } +} diff --git a/FFMpegCore/FFMPEG/Arguments/SeekArgument.cs b/FFMpegCore/FFMPEG/Arguments/SeekArgument.cs new file mode 100644 index 0000000..e67c934 --- /dev/null +++ b/FFMpegCore/FFMPEG/Arguments/SeekArgument.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FFMpegCore.FFMPEG.Arguments +{ + /// + /// Represents seek parameter + /// + public class SeekArgument : Argument + { + public SeekArgument() + { + } + + public SeekArgument(TimeSpan value) : base(value) + { + } + + /// + /// String representation of the argument + /// + /// String representation of the argument + public override string GetStringValue() + { + return ArgumentsStringifier.Seek(Value); + } + } +} diff --git a/FFMpegCore/FFMPEG/Arguments/ShortestArgument.cs b/FFMpegCore/FFMPEG/Arguments/ShortestArgument.cs new file mode 100644 index 0000000..cc04604 --- /dev/null +++ b/FFMpegCore/FFMPEG/Arguments/ShortestArgument.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FFMpegCore.FFMPEG.Arguments +{ + /// + /// Represents shortest parameter + /// + public class ShortestArgument : Argument + { + public ShortestArgument() + { + } + + public ShortestArgument(bool value) : base(value) + { + } + + /// + /// String representation of the argument + /// + /// String representation of the argument + public override string GetStringValue() + { + return ArgumentsStringifier.FinalizeAtShortestInput(Value); + } + } +} diff --git a/FFMpegCore/FFMPEG/Arguments/SizeArgument.cs b/FFMpegCore/FFMPEG/Arguments/SizeArgument.cs new file mode 100644 index 0000000..b9eed0c --- /dev/null +++ b/FFMpegCore/FFMPEG/Arguments/SizeArgument.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FFMpegCore.FFMPEG.Enums; + +namespace FFMpegCore.FFMPEG.Arguments +{ + /// + /// Represents size parameter + /// + public class SizeArgument : ScaleArgument + { + public SizeArgument() + { + } + + public SizeArgument(Size value) : base(value) + { + } + + public SizeArgument(VideoSize videosize) : base(videosize) + { + } + + public SizeArgument(int width, int heignt) : base(width, heignt) + { + } + + /// + /// String representation of the argument + /// + /// String representation of the argument + public override string GetStringValue() + { + return ArgumentsStringifier.Size(Value); + } + } +} diff --git a/FFMpegCore/FFMPEG/Arguments/SpeedArgument.cs b/FFMpegCore/FFMPEG/Arguments/SpeedArgument.cs new file mode 100644 index 0000000..9801f8a --- /dev/null +++ b/FFMpegCore/FFMPEG/Arguments/SpeedArgument.cs @@ -0,0 +1,32 @@ +using FFMpegCore.FFMPEG.Enums; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FFMpegCore.FFMPEG.Arguments +{ + /// + /// Represents speed parameter + /// + public class SpeedArgument : Argument + { + public SpeedArgument() + { + } + + public SpeedArgument(Speed value) : base(value) + { + } + + /// + /// String representation of the argument + /// + /// String representation of the argument + public override string GetStringValue() + { + return ArgumentsStringifier.Speed(Value); + } + } +} diff --git a/FFMpegCore/FFMPEG/Arguments/StartNumberArgument.cs b/FFMpegCore/FFMPEG/Arguments/StartNumberArgument.cs new file mode 100644 index 0000000..65c66ee --- /dev/null +++ b/FFMpegCore/FFMPEG/Arguments/StartNumberArgument.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FFMpegCore.FFMPEG.Arguments +{ + /// + /// Represents start number parameter + /// + public class StartNumberArgument : Argument + { + public StartNumberArgument() + { + } + + public StartNumberArgument(int value) : base(value) + { + } + + /// + /// String representation of the argument + /// + /// String representation of the argument + public override string GetStringValue() + { + return ArgumentsStringifier.StartNumber(Value); + } + } +} diff --git a/FFMpegCore/FFMPEG/Arguments/ThreadsArgument.cs b/FFMpegCore/FFMPEG/Arguments/ThreadsArgument.cs new file mode 100644 index 0000000..6bd3f6b --- /dev/null +++ b/FFMpegCore/FFMPEG/Arguments/ThreadsArgument.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FFMpegCore.FFMPEG.Arguments +{ + /// + /// Represents threads parameter + /// Number of threads used for video encoding + /// + public class ThreadsArgument : Argument + { + public ThreadsArgument() + { + } + + public ThreadsArgument(int value) : base(value) + { + } + + public ThreadsArgument(bool isMultiThreaded) : + base(isMultiThreaded + ? Environment.ProcessorCount + : 1) + { + } + + /// + /// String representation of the argument + /// + /// String representation of the argument + public override string GetStringValue() + { + return ArgumentsStringifier.Threads(Value); + } + } +} diff --git a/FFMpegCore/FFMPEG/Arguments/VideoCodecArgument.cs b/FFMpegCore/FFMPEG/Arguments/VideoCodecArgument.cs new file mode 100644 index 0000000..cb0217a --- /dev/null +++ b/FFMpegCore/FFMPEG/Arguments/VideoCodecArgument.cs @@ -0,0 +1,39 @@ +using FFMpegCore.FFMPEG.Enums; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace FFMpegCore.FFMPEG.Arguments +{ + /// + /// Represents video codec parameter + /// + public class VideoCodecArgument : Argument + { + public int Bitrate { get; protected set; } = 0; + + public VideoCodecArgument() + { + } + + public VideoCodecArgument(VideoCodec value) : base(value) + { + } + + public VideoCodecArgument(VideoCodec value, int bitrate) : base(value) + { + Bitrate = bitrate; + } + + /// + /// String representation of the argument + /// + /// String representation of the argument + public override string GetStringValue() + { + return ArgumentsStringifier.Video(Value, Bitrate); + } + } +} diff --git a/FFMpegCore/FFMPEG/Enums/AudioQuality.cs b/FFMpegCore/FFMPEG/Enums/AudioQuality.cs new file mode 100644 index 0000000..d2b9465 --- /dev/null +++ b/FFMpegCore/FFMPEG/Enums/AudioQuality.cs @@ -0,0 +1,10 @@ +namespace FFMpegCore.FFMPEG.Enums +{ + public enum AudioQuality + { + Ultra = 384, + Hd = 192, + Normal = 128, + Low = 64 + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMPEG/Enums/Codec.cs b/FFMpegCore/FFMPEG/Enums/Codec.cs new file mode 100644 index 0000000..b0aaf60 --- /dev/null +++ b/FFMpegCore/FFMPEG/Enums/Codec.cs @@ -0,0 +1,30 @@ +namespace FFMpegCore.FFMPEG.Enums +{ + public enum VideoCodec + { + LibX264, + LibVpx, + LibTheora, + Png, + MpegTs + } + + public enum AudioCodec + { + Aac, + LibVorbis + } + + public enum Filter + { + H264_Mp4ToAnnexB, + Aac_AdtstoAsc + } + + public enum Channel + { + Audio, + Video, + Both + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMPEG/Enums/Speed.cs b/FFMpegCore/FFMPEG/Enums/Speed.cs new file mode 100644 index 0000000..089ed9c --- /dev/null +++ b/FFMpegCore/FFMPEG/Enums/Speed.cs @@ -0,0 +1,15 @@ +namespace FFMpegCore.FFMPEG.Enums +{ + public enum Speed + { + VerySlow, + Slower, + Slow, + Medium, + Fast, + Faster, + VeryFast, + SuperFast, + UltraFast + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMPEG/Enums/VideoSize.cs b/FFMpegCore/FFMPEG/Enums/VideoSize.cs new file mode 100644 index 0000000..396d349 --- /dev/null +++ b/FFMpegCore/FFMPEG/Enums/VideoSize.cs @@ -0,0 +1,11 @@ +namespace FFMpegCore.FFMPEG.Enums +{ + public enum VideoSize + { + Hd = 720, + FullHd = 1080, + Ed = 480, + Ld = 360, + Original + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMPEG/Exceptions/FFMpegException.cs b/FFMpegCore/FFMPEG/Exceptions/FFMpegException.cs new file mode 100644 index 0000000..ca927d0 --- /dev/null +++ b/FFMpegCore/FFMPEG/Exceptions/FFMpegException.cs @@ -0,0 +1,31 @@ +using System; +using System.Text; + +namespace FFMpegCore.FFMPEG.Exceptions +{ + public enum FFMpegExceptionType + { + Dependency, + Conversion, + File, + Operation, + Process + } + + public class FFMpegException : Exception + { + public FFMpegException(FFMpegExceptionType type): this(type, null, null) { } + + public FFMpegException(FFMpegExceptionType type, StringBuilder sb): this(type, sb.ToString(), null) { } + + public FFMpegException(FFMpegExceptionType type, string message): this(type, message, null) { } + + public FFMpegException(FFMpegExceptionType type, string message, FFMpegException innerException) + : base(message, innerException) + { + Type = type; + } + + public FFMpegExceptionType Type { get; set; } + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMPEG/FFBase.cs b/FFMpegCore/FFMPEG/FFBase.cs new file mode 100644 index 0000000..03bb3d7 --- /dev/null +++ b/FFMpegCore/FFMPEG/FFBase.cs @@ -0,0 +1,81 @@ +using System; +using System.Diagnostics; +using System.Linq; + +namespace FFMpegCore.FFMPEG +{ + public abstract class FFBase : IDisposable + { + protected string ConfiguredRoot; + protected Process Process; + + protected FFBase() + { + ConfiguredRoot = ".\\FFMPEG\\bin"; + } + + /// + /// Is 'true' when an exception is thrown during process kill (for paranoia level users). + /// + public bool IsKillFaulty { get; private set; } + + /// + /// Returns true if the associated process is still alive/running. + /// + public bool IsWorking + { + get + { + bool processHasExited; + + try + { + processHasExited = Process.HasExited; + } + catch + { + processHasExited = true; + } + + return !processHasExited && Process.GetProcesses().Any(x => x.Id == Process.Id); + } + } + + public void Dispose() + { + Process?.Dispose(); + } + + protected void CreateProcess(string args, string processPath, bool rStandardInput = false, + bool rStandardOutput = false, bool rStandardError = false) + { + if (IsWorking) + throw new InvalidOperationException( + "The current FFMpeg process is busy with another operation. Create a new object for parallel executions."); + + Process = new Process(); + IsKillFaulty = false; + Process.StartInfo.FileName = processPath; + Process.StartInfo.Arguments = args; + Process.StartInfo.UseShellExecute = false; + Process.StartInfo.CreateNoWindow = true; + + Process.StartInfo.RedirectStandardInput = rStandardInput; + Process.StartInfo.RedirectStandardOutput = rStandardOutput; + Process.StartInfo.RedirectStandardError = rStandardError; + } + + public void Kill() + { + try + { + if (IsWorking) + Process.Kill(); + } + catch + { + IsKillFaulty = true; + } + } + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMPEG/FFMpeg.cs b/FFMpegCore/FFMPEG/FFMpeg.cs new file mode 100644 index 0000000..6182a86 --- /dev/null +++ b/FFMpegCore/FFMPEG/FFMpeg.cs @@ -0,0 +1,543 @@ +using FFMpegCore.Enums; +using FFMpegCore.FFMPEG.Arguments; +using FFMpegCore.FFMPEG.Enums; +using FFMpegCore.FFMPEG.Exceptions; +using FFMpegCore.Helpers; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.Drawing.Imaging; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace FFMpegCore.FFMPEG +{ + public delegate void ConversionHandler(double percentage); + + public class FFMpeg : FFBase + { + IArgumentBuilder argumentBuilder { get; set; } + + /// + /// Intializes the FFMPEG encoder. + /// + public FFMpeg() + { + _Init(); + argumentBuilder = new FFArgumentBuilder(); + } + + public FFMpeg(IArgumentBuilder builder) + { + _Init(); + argumentBuilder = builder; + } + + private void _Init() + { + FFMpegHelper.RootExceptionCheck(ConfiguredRoot); + FFProbeHelper.RootExceptionCheck(ConfiguredRoot); + + var target = Environment.Is64BitProcess ? "x64" : "x86"; + + _ffmpegPath = ConfiguredRoot + $"\\{target}\\ffmpeg.exe"; + } + + /// + /// Returns the percentage of the current conversion progress. + /// + public event ConversionHandler OnProgress; + + /// + /// Saves a 'png' thumbnail from the input video. + /// + /// Source video file. + /// Output video file + /// Seek position where the thumbnail should be taken. + /// Thumbnail size. If width or height equal 0, the other will be computed automatically. + /// Bitmap with the requested snapshot. + public Bitmap Snapshot(VideoInfo source, FileInfo output, Size? size = null, TimeSpan? captureTime = null) + { + if (captureTime == null) + captureTime = TimeSpan.FromSeconds(source.Duration.TotalSeconds / 3); + + if (output.Extension.ToLower() != FileExtension.Png) + output = new FileInfo(output.FullName.Replace(output.Extension, FileExtension.Png)); + + if (size == null || (size.Value.Height == 0 && size.Value.Width == 0)) + { + size = new Size(source.Width, source.Height); + } + + if (size.Value.Width != size.Value.Height) + { + if (size.Value.Width == 0) + { + var ratio = source.Width / (double)size.Value.Width; + + size = new Size((int)(source.Width * ratio), (int)(source.Height * ratio)); + } + + if (size.Value.Height == 0) + { + var ratio = source.Height / (double)size.Value.Height; + + size = new Size((int)(source.Width * ratio), (int)(source.Height * ratio)); + } + } + + FFMpegHelper.ConversionExceptionCheck(source.ToFileInfo(), output); + + var thumbArgs = ArgumentsStringifier.Input(source) + + ArgumentsStringifier.Video(VideoCodec.Png) + + ArgumentsStringifier.FrameOutputCount(1) + + ArgumentsStringifier.Seek(captureTime) + + ArgumentsStringifier.Size(size) + + ArgumentsStringifier.Output(output); + + if (!RunProcess(thumbArgs, output)) + { + throw new OperationCanceledException("Could not take snapshot!"); + } + + output.Refresh(); + + Bitmap result; + using (var bmp = (Bitmap)Image.FromFile(output.FullName)) + { + using (var ms = new MemoryStream()) + { + bmp.Save(ms, ImageFormat.Png); + result = new Bitmap(ms); + } + } + + if (output.Exists) + { + output.Delete(); + } + + return result; + } + + /// + /// Convert a video do a different format. + /// + /// Input video source. + /// Output information. + /// Target conversion video type. + /// Conversion target speed/quality (faster speed = lower quality). + /// Video size. + /// Conversion target audio quality. + /// Is encoding multithreaded. + /// Output video information. + public VideoInfo Convert( + VideoInfo source, + FileInfo output, + VideoType type = VideoType.Mp4, + Speed speed = + Speed.SuperFast, + VideoSize size = + VideoSize.Original, + AudioQuality audioQuality = AudioQuality.Normal, + bool multithreaded = false) + { + FFMpegHelper.ConversionExceptionCheck(source.ToFileInfo(), output); + FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.ForType(type)); + FFMpegHelper.ConversionSizeExceptionCheck(source); + + string args = ""; + + var scale = VideoSize.Original == size ? 1 : + (double)source.Height / (int)size; + + var outputSize = new Size( + (int)(source.Width / scale), + (int)(source.Height / scale) + ); + + if (outputSize.Width % 2 != 0) + { + outputSize.Width += 1; + } + + switch (type) + { + case VideoType.Mp4: + + args = ArgumentsStringifier.Input(source) + + ArgumentsStringifier.Threads(multithreaded) + + ArgumentsStringifier.Scale(outputSize) + + ArgumentsStringifier.Video(VideoCodec.LibX264, 2400) + + ArgumentsStringifier.Speed(speed) + + ArgumentsStringifier.Audio(AudioCodec.Aac, audioQuality) + + ArgumentsStringifier.Output(output); + break; + case VideoType.Ogv: + args = ArgumentsStringifier.Input(source) + + ArgumentsStringifier.Threads(multithreaded) + + ArgumentsStringifier.Scale(outputSize) + + ArgumentsStringifier.Video(VideoCodec.LibTheora, 2400) + + ArgumentsStringifier.Speed(16) + + ArgumentsStringifier.Audio(AudioCodec.LibVorbis, audioQuality) + + ArgumentsStringifier.Output(output); + + break; + case VideoType.Ts: + args = ArgumentsStringifier.Input(source) + + ArgumentsStringifier.Copy() + + ArgumentsStringifier.BitStreamFilter(Channel.Video, Filter.H264_Mp4ToAnnexB) + + ArgumentsStringifier.ForceFormat(VideoCodec.MpegTs) + + ArgumentsStringifier.Output(output); + break; + } + + if (!RunProcess(args, output)) + { + throw new FFMpegException(FFMpegExceptionType.Conversion, $"The video could not be converted to {Enum.GetName(typeof(VideoType), type)}"); + } + + return new VideoInfo(output); + } + + /// + /// Adds a poster image to an audio file. + /// + /// Source image file. + /// Source audio file. + /// Output video file. + /// + public VideoInfo PosterWithAudio(FileInfo image, FileInfo audio, FileInfo output) + { + FFMpegHelper.InputsExistExceptionCheck(image, audio); + FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.Mp4); + FFMpegHelper.ConversionSizeExceptionCheck(Image.FromFile(image.FullName)); + + var args = ArgumentsStringifier.Loop(1) + + ArgumentsStringifier.Input(image) + + ArgumentsStringifier.Input(audio) + + ArgumentsStringifier.Video(VideoCodec.LibX264, 2400) + + ArgumentsStringifier.Audio(AudioCodec.Aac, AudioQuality.Normal) + + ArgumentsStringifier.FinalizeAtShortestInput(true) + + ArgumentsStringifier.Output(output); + + if (!RunProcess(args, output)) + { + throw new FFMpegException(FFMpegExceptionType.Operation, "An error occured while adding the audio file to the image."); + } + + return new VideoInfo(output); + } + + /// + /// Joins a list of video files. + /// + /// Output video file. + /// List of vides that need to be joined together. + /// Output video information. + public VideoInfo Join(FileInfo output, params VideoInfo[] videos) + { + FFMpegHelper.OutputExistsExceptionCheck(output); + FFMpegHelper.InputsExistExceptionCheck(videos.Select(video => video.ToFileInfo()).ToArray()); + + var temporaryVideoParts = videos.Select(video => + { + FFMpegHelper.ConversionSizeExceptionCheck(video); + var destinationPath = video.FullName.Replace(video.Extension, FileExtension.Ts); + Convert( + video, + new FileInfo(destinationPath), + VideoType.Ts + ); + return destinationPath; + }).ToList(); + + var args = ArgumentsStringifier.InputConcat(temporaryVideoParts) + + ArgumentsStringifier.Copy() + + ArgumentsStringifier.BitStreamFilter(Channel.Audio, Filter.Aac_AdtstoAsc) + + ArgumentsStringifier.Output(output); + + try + { + if (!RunProcess(args, output)) + { + throw new FFMpegException(FFMpegExceptionType.Operation, "Could not join the provided video files."); + } + return new VideoInfo(output); + + } + finally + { + Cleanup(temporaryVideoParts); + } + } + + /// + /// Converts an image sequence to a video. + /// + /// Output video file. + /// FPS + /// Image sequence collection + /// Output video information. + public VideoInfo JoinImageSequence(FileInfo output, double frameRate = 30, params ImageInfo[] images) + { + var temporaryImageFiles = images.Select((image, index) => + { + FFMpegHelper.ConversionSizeExceptionCheck(Image.FromFile(image.FullName)); + var destinationPath = image.FullName.Replace(image.Name, $"{index.ToString().PadLeft(9, '0')}{image.Extension}"); + File.Copy(image.FullName, destinationPath); + + return destinationPath; + }).ToList(); + + var firstImage = images.First(); + + var args = ArgumentsStringifier.FrameRate(frameRate) + + ArgumentsStringifier.Size(new Size(firstImage.Width, firstImage.Height)) + + ArgumentsStringifier.StartNumber(0) + + ArgumentsStringifier.Input($"{firstImage.Directory}\\%09d.png") + + ArgumentsStringifier.FrameOutputCount(images.Length) + + ArgumentsStringifier.Video(VideoCodec.LibX264) + + ArgumentsStringifier.Output(output); + + try + { + if (!RunProcess(args, output)) + { + throw new FFMpegException(FFMpegExceptionType.Operation, "Could not join the provided image sequence."); + } + + return new VideoInfo(output); + } + finally + { + Cleanup(temporaryImageFiles); + } + } + + /// + /// Records M3U8 streams to the specified output. + /// + /// URI to pointing towards stream. + /// Output file + /// Success state. + public VideoInfo SaveM3U8Stream(Uri uri, FileInfo output) + { + FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.Mp4); + + if (uri.Scheme == "http" || uri.Scheme == "https") + { + var args = ArgumentsStringifier.Input(uri) + + ArgumentsStringifier.Output(output); + + if (!RunProcess(args, output)) + { + throw new FFMpegException(FFMpegExceptionType.Operation, $"Saving the ${uri.AbsoluteUri} stream failed."); + } + + return new VideoInfo(output); + } + throw new ArgumentException($"Uri: {uri.AbsoluteUri}, does not point to a valid http(s) stream."); + } + + /// + /// Strips a video file of audio. + /// + /// Source video file. + /// Output video file. + /// + public VideoInfo Mute(VideoInfo source, FileInfo output) + { + FFMpegHelper.ConversionExceptionCheck(source.ToFileInfo(), output); + FFMpegHelper.ConversionSizeExceptionCheck(source); + FFMpegHelper.ExtensionExceptionCheck(output, source.Extension); + + var args = ArgumentsStringifier.Input(source) + + ArgumentsStringifier.Copy() + + ArgumentsStringifier.Disable(Channel.Audio) + + ArgumentsStringifier.Output(output); + + if (!RunProcess(args, output)) + { + throw new FFMpegException(FFMpegExceptionType.Operation, "Could not mute the requested video."); + } + + return new VideoInfo(output); + } + + /// + /// Saves audio from a specific video file to disk. + /// + /// Source video file. + /// Output audio file. + /// Success state. + public FileInfo ExtractAudio(VideoInfo source, FileInfo output) + { + FFMpegHelper.ConversionExceptionCheck(source.ToFileInfo(), output); + FFMpegHelper.ExtensionExceptionCheck(output, FileExtension.Mp3); + + var args = ArgumentsStringifier.Input(source) + + ArgumentsStringifier.Disable(Channel.Video) + + ArgumentsStringifier.Output(output); + + if (!RunProcess(args, output)) + { + throw new FFMpegException(FFMpegExceptionType.Operation, "Could not extract the audio from the requested video."); + } + + output.Refresh(); + + return output; + } + + /// + /// Adds audio to a video file. + /// + /// Source video file. + /// Source audio file. + /// Output video file. + /// Indicates if the encoding should stop at the shortest input file. + /// Success state + public VideoInfo ReplaceAudio(VideoInfo source, FileInfo audio, FileInfo output, bool stopAtShortest = false) + { + FFMpegHelper.ConversionExceptionCheck(source.ToFileInfo(), output); + FFMpegHelper.InputsExistExceptionCheck(audio); + FFMpegHelper.ConversionSizeExceptionCheck(source); + FFMpegHelper.ExtensionExceptionCheck(output, source.Extension); + + var args = ArgumentsStringifier.Input(source) + + ArgumentsStringifier.Input(audio) + + ArgumentsStringifier.Copy(Channel.Video) + + ArgumentsStringifier.Audio(AudioCodec.Aac, AudioQuality.Hd) + + ArgumentsStringifier.FinalizeAtShortestInput(stopAtShortest) + + ArgumentsStringifier.Output(output); + + if (!RunProcess(args, output)) + { + throw new FFMpegException(FFMpegExceptionType.Operation, "Could not replace the video audio."); + } + + return new VideoInfo(output); + } + + public VideoInfo Convert(ArgumentsContainer arguments) + { + var args = argumentBuilder.BuildArguments(arguments); + var output = ((OutputArgument)arguments[typeof(OutputArgument)]).GetAsFileInfo(); + + if (!RunProcess(args, output)) + { + throw new FFMpegException(FFMpegExceptionType.Operation, "Could not replace the video audio."); + } + + return new VideoInfo(output); + } + + public VideoInfo Convert(ArgumentsContainer arguments, FileInfo input, FileInfo output) + { + var args = argumentBuilder.BuildArguments(arguments, input, output); + + if (!RunProcess(args, output)) + { + throw new FFMpegException(FFMpegExceptionType.Operation, "Could not replace the video audio."); + } + + return new VideoInfo(output); + } + + /// + /// Stops any current job that FFMpeg is running. + /// + public void Stop() + { + if (IsWorking) + { + Process.StandardInput.Write('q'); + } + } + + #region Private Members & Methods + + private string _ffmpegPath; + private TimeSpan _totalTime; + + private volatile StringBuilder _errorOutput = new StringBuilder(); + + private bool RunProcess(string args, FileInfo output) + { + var successState = true; + + CreateProcess(args, _ffmpegPath, true, rStandardError: true); + + try + { + Process.Start(); + Process.ErrorDataReceived += OutputData; + Process.BeginErrorReadLine(); + Process.WaitForExit(); + } + catch (Exception) + { + successState = false; + } + finally + { + Process.Close(); + + if (File.Exists(output.FullName)) + using (var file = File.Open(output.FullName, FileMode.Open)) + { + if (file.Length == 0) + { + throw new FFMpegException(FFMpegExceptionType.Process, _errorOutput); + } + } + else + { + throw new FFMpegException(FFMpegExceptionType.Process, _errorOutput); + } + } + return successState; + } + + private void Cleanup(IEnumerable pathList) + { + foreach (var path in pathList) + { + if (File.Exists(path)) + { + File.Delete(path); + } + } + } + + private void OutputData(object sender, DataReceivedEventArgs e) + { + if (e.Data == null) + return; + + _errorOutput.AppendLine(e.Data); +#if DEBUG + Trace.WriteLine(e.Data); +#endif + + if (OnProgress == null || !IsWorking) return; + + var r = new Regex(@"\w\w:\w\w:\w\w"); + var m = r.Match(e.Data); + + if (!e.Data.Contains("frame")) return; + if (!m.Success) return; + + var t = TimeSpan.Parse(m.Value, CultureInfo.InvariantCulture); + var percentage = Math.Round(t.TotalSeconds / _totalTime.TotalSeconds * 100, 2); + OnProgress(percentage); + } + + #endregion + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMPEG/FFProbe.cs b/FFMpegCore/FFMPEG/FFProbe.cs new file mode 100644 index 0000000..06801f6 --- /dev/null +++ b/FFMpegCore/FFMPEG/FFProbe.cs @@ -0,0 +1,138 @@ +using FFMpegCore.Helpers; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace FFMpegCore.FFMPEG +{ + public sealed class FFProbe : FFBase + { + public FFProbe() + { + FFProbeHelper.RootExceptionCheck(ConfiguredRoot); + + var target = Environment.Is64BitProcess ? "x64" : "x86"; + + _ffprobePath = ConfiguredRoot + $"\\{target}\\ffprobe.exe"; + } + + /// + /// Probes the targeted video file and retrieves all available details. + /// + /// Source video file. + /// A video info object containing all details necessary. + public VideoInfo ParseVideoInfo(string source) + { + return ParseVideoInfo(new VideoInfo(source)); + } + + /// + /// Probes the targeted video file and retrieves all available details. + /// + /// Source video file. + /// A video info object containing all details necessary. + public VideoInfo ParseVideoInfo(VideoInfo info) + { + var jsonOutput = + RunProcess($"-v quiet -print_format json -show_streams \"{info.FullName}\""); + + var metadata = JsonConvert.DeserializeObject>(jsonOutput); + int videoIndex = metadata["streams"][0]["codec_type"] == "video" ? 0 : 1, + audioIndex = 1 - videoIndex; + + var bitRate = Convert.ToDouble(metadata["streams"][videoIndex]["bit_rate"], CultureInfo.InvariantCulture); + + try + { + var duration = Convert.ToDouble(metadata["streams"][videoIndex]["duration"], CultureInfo.InvariantCulture); + info.Duration = TimeSpan.FromSeconds(duration); + info.Duration = info.Duration.Subtract(TimeSpan.FromMilliseconds(info.Duration.Milliseconds)); + } + catch (Exception) + { + info.Duration = TimeSpan.FromSeconds(0); + } + + + // Get video size in megabytes + double videoSize = 0, + audioSize = 0; + + try + { + info.VideoFormat = metadata["streams"][videoIndex]["codec_name"]; + videoSize = bitRate * info.Duration / 8388608; + } + catch (Exception) + { + info.VideoFormat = "none"; + } + + // Get audio format - wrap for exceptions if the video has no audio + try + { + info.AudioFormat = metadata["streams"][audioIndex]["codec_name"]; + audioSize = bitRate * info.Duration / 8388608; + } + catch (Exception) + { + info.AudioFormat = "none"; + } + + // Get video format + + + // Get video width + info.Width = metadata["streams"][videoIndex]["width"]; + + // Get video height + info.Height = metadata["streams"][videoIndex]["height"]; + + info.Size = Math.Round(videoSize + audioSize, 2); + + // Get video aspect ratio + var cd = FFProbeHelper.Gcd(info.Width, info.Height); + info.Ratio = info.Width / cd + ":" + info.Height / cd; + + // Get video framerate + var fr = ((string)metadata["streams"][videoIndex]["r_frame_rate"]).Split('/'); + info.FrameRate = Math.Round( + Convert.ToDouble(fr[0], CultureInfo.InvariantCulture) / + Convert.ToDouble(fr[1], CultureInfo.InvariantCulture), + 3); + + return info; + } + + #region Private Members & Methods + + private readonly string _ffprobePath; + + private string RunProcess(string args) + { + CreateProcess(args, _ffprobePath, rStandardOutput: true); + + string output; + + try + { + Process.Start(); + output = Process.StandardOutput.ReadToEnd(); + } + catch (Exception) + { + output = ""; + } + finally + { + Process.WaitForExit(); + Process.Close(); + } + + return output; + } + + #endregion + } +} \ No newline at end of file diff --git a/FFMpegCore/FFMpegCore.csproj b/FFMpegCore/FFMpegCore.csproj new file mode 100644 index 0000000..ec03804 --- /dev/null +++ b/FFMpegCore/FFMpegCore.csproj @@ -0,0 +1,144 @@ + + + + netstandard2.0 + en + https://github.com/vladjerca/FFMpegCore + https://github.com/vladjerca/FFMpegCore + Vlad Jerca + A great way to use FFMpeg encoding when writing video applications, client-side and server-side. It has wrapper methods that allow conversion to all web formats: MP4, OGV, TS and methods of capturing screens from the videos. + + + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + + + + + + + + diff --git a/FFMpegCore/FFMpegCore.nuspec b/FFMpegCore/FFMpegCore.nuspec new file mode 100644 index 0000000..cea1009 --- /dev/null +++ b/FFMpegCore/FFMpegCore.nuspec @@ -0,0 +1,21 @@ + + + + $id$ + $version$ + $title$ + Vlad Jerca + Vlad Jerca + https://github.com/vladjerca/FFMpegCore + false + $description$ + + More information available @ https://github.com/vladjerca/FFMpegCore/blob/master/README.md + + + + + Copyright 2019 + ffmpeg video conversion FFMpegCore mp4 ogv net.core core net + + diff --git a/FFMpegCore/Helpers/FFMpegHelper.cs b/FFMpegCore/Helpers/FFMpegHelper.cs new file mode 100644 index 0000000..3f33471 --- /dev/null +++ b/FFMpegCore/Helpers/FFMpegHelper.cs @@ -0,0 +1,85 @@ +using System; +using System.Drawing; +using System.IO; +using FFMpegCore.FFMPEG.Exceptions; + +namespace FFMpegCore.Helpers +{ + public static class FFMpegHelper + { + public static void ConversionSizeExceptionCheck(Image image) + { + ConversionSizeExceptionCheck(image.Size); + } + + public static void ConversionSizeExceptionCheck(VideoInfo info) + { + ConversionSizeExceptionCheck(new Size(info.Width, info.Height)); + } + + public static void ConversionSizeExceptionCheck(Size size) + { + if ( + size.Height % 2 != 0 || + size.Width % 2 != 0 + ) + { + throw new ArgumentException("FFMpeg yuv420p encoding requires the width and height to be a multiple of 2!"); + } + } + + public static void OutputExistsExceptionCheck(FileInfo output) + { + if (File.Exists(output.FullName)) + { + throw new FFMpegException(FFMpegExceptionType.File, + $"The output file: {output} already exists!"); + } + } + + public static void InputExistsExceptionCheck(FileInfo input) + { + if (!File.Exists(input.FullName)) + { + throw new FFMpegException(FFMpegExceptionType.File, + $"Input {input.FullName} does not exist!"); + } + } + + public static void ConversionExceptionCheck(FileInfo originalVideo, FileInfo convertedPath) + { + OutputExistsExceptionCheck(convertedPath); + InputExistsExceptionCheck(originalVideo); + } + + public static void InputsExistExceptionCheck(params FileInfo[] paths) + { + foreach (var path in paths) + { + InputExistsExceptionCheck(path); + } + } + + public static void ExtensionExceptionCheck(FileInfo output, string expected) + { + if (!expected.Equals(new FileInfo(output.FullName).Extension, StringComparison.OrdinalIgnoreCase)) + throw new FFMpegException(FFMpegExceptionType.File, + $"Invalid output file. File extension should be '{expected}' required."); + } + + public static void RootExceptionCheck(string root) + { + if (root == null) + throw new FFMpegException(FFMpegExceptionType.Dependency, + "FFMpeg root is not configured in app config. Missing key 'ffmpegRoot'."); + + var target = Environment.Is64BitProcess ? "x64" : "x86"; + + var path = root + $"\\{target}\\ffmpeg.exe"; + + if (!File.Exists(path)) + throw new FFMpegException(FFMpegExceptionType.Dependency, + "FFMpeg cannot be found in the root directory!"); + } + } +} \ No newline at end of file diff --git a/FFMpegCore/Helpers/FFProbeHelper.cs b/FFMpegCore/Helpers/FFProbeHelper.cs new file mode 100644 index 0000000..749f311 --- /dev/null +++ b/FFMpegCore/Helpers/FFProbeHelper.cs @@ -0,0 +1,35 @@ +using System; +using System.IO; +using FFMpegCore.FFMPEG.Exceptions; + +namespace FFMpegCore.Helpers +{ + public class FFProbeHelper + { + public static int Gcd(int first, int second) + { + while (first != 0 && second != 0) + { + if (first > second) + first -= second; + else second -= first; + } + return first == 0 ? second : first; + } + + public static void RootExceptionCheck(string root) + { + if (root == null) + throw new FFMpegException(FFMpegExceptionType.Dependency, + "FFProbe root is not configured in app config. Missing key 'ffmpegRoot'."); + + var target = Environment.Is64BitProcess ? "x64" : "x86"; + + var path = root + $"\\{target}\\ffprobe.exe"; + + if (!File.Exists(path)) + throw new FFMpegException(FFMpegExceptionType.Dependency, + $"FFProbe cannot be found in the in {path}..."); + } + } +} \ No newline at end of file diff --git a/FFMpegCore/ImageInfo.cs b/FFMpegCore/ImageInfo.cs new file mode 100644 index 0000000..a8551c4 --- /dev/null +++ b/FFMpegCore/ImageInfo.cs @@ -0,0 +1,183 @@ +using FFMpegCore.Enums; +using FFMpegCore.FFMPEG; +using FFMpegCore.Helpers; +using System; +using System.Drawing; +using System.IO; +using System.Linq; + +namespace FFMpegCore +{ + public class ImageInfo + { + private FileInfo _file; + + /// + /// Create a image information object from a target path. + /// + /// Image file information. + public ImageInfo(FileInfo fileInfo) + { + if (!fileInfo.Extension.ToLower().EndsWith(FileExtension.Png)) + { + throw new Exception("Image joining currently suppors only .png file types"); + } + + fileInfo.Refresh(); + + this.Size = fileInfo.Length / (1024 * 1024); + + using (var image = Image.FromFile(fileInfo.FullName)) + { + this.Width = image.Width; + this.Height = image.Height; + var cd = FFProbeHelper.Gcd(this.Width, this.Height); + this.Ratio = $"{this.Width / cd}:{this.Height / cd}"; + } + + + if (!fileInfo.Exists) + throw new ArgumentException($"Input file {fileInfo.FullName} does not exist!"); + + _file = fileInfo; + + + } + + /// + /// Create a image information object from a target path. + /// + /// Path to image. + public ImageInfo(string path) : this(new FileInfo(path)) + { + } + + /// + /// Aspect ratio. + /// + public string Ratio { get; internal set; } + + /// + /// Height of the image file. + /// + public int Height { get; internal set; } + + /// + /// Width of the image file. + /// + public int Width { get; internal set; } + + /// + /// Image file size in MegaBytes (MB). + /// + public double Size { get; internal set; } + + /// + /// Gets the name of the file. + /// + public string Name => _file.Name; + + /// + /// Gets the full path of the file. + /// + public string FullName => _file.FullName; + + /// + /// Gets the file extension. + /// + public string Extension => _file.Extension; + + /// + /// Gets a flag indicating if the file is read-only. + /// + public bool IsReadOnly => _file.IsReadOnly; + + /// + /// Gets a flag indicating if the file exists (no cache, per call verification). + /// + public bool Exists => File.Exists(FullName); + + /// + /// Gets the creation date. + /// + public DateTime CreationTime => _file.CreationTime; + + /// + /// Gets the parent directory information. + /// + public DirectoryInfo Directory => _file.Directory; + + /// + /// Create a image information object from a file information object. + /// + /// Image file information. + /// + public static ImageInfo FromFileInfo(FileInfo fileInfo) + { + return FromPath(fileInfo.FullName); + } + + /// + /// Create a image information object from a target path. + /// + /// Path to image. + /// + public static ImageInfo FromPath(string path) + { + return new ImageInfo(path); + } + + /// + /// Pretty prints the image information. + /// + /// + public override string ToString() + { + return "Image Path : " + FullName + Environment.NewLine + + "Image Root : " + Directory.FullName + Environment.NewLine + + "Image Name: " + Name + Environment.NewLine + + "Image Extension : " + Extension + Environment.NewLine + + "Aspect Ratio : " + Ratio + Environment.NewLine + + "Resolution : " + Width + "x" + Height + Environment.NewLine + + "Size : " + Size + " MB"; + } + + /// + /// Open a file stream. + /// + /// Opens a file in a specified mode. + /// File stream of the image file. + public FileStream FileOpen(FileMode mode) + { + return _file.Open(mode); + } + + /// + /// Move file to a specific directory. + /// + /// + public void MoveTo(DirectoryInfo destination) + { + var newLocation = $"{destination.FullName}\\{Name}{Extension}"; + _file.MoveTo(newLocation); + _file = new FileInfo(newLocation); + } + + /// + /// Delete the file. + /// + public void Delete() + { + _file.Delete(); + } + + /// + /// Converts image info to file info. + /// + /// A new FileInfo instance. + public FileInfo ToFileInfo() + { + return new FileInfo(_file.FullName); + } + } +} \ No newline at end of file diff --git a/FFMpegCore/VideoInfo.cs b/FFMpegCore/VideoInfo.cs new file mode 100644 index 0000000..13eebf2 --- /dev/null +++ b/FFMpegCore/VideoInfo.cs @@ -0,0 +1,187 @@ +using FFMpegCore.FFMPEG; +using System; +using System.IO; + +namespace FFMpegCore +{ + public class VideoInfo + { + private FileInfo _file; + + /// + /// Create a video information object from a file information object. + /// + /// Video file information. + public VideoInfo(FileInfo fileInfo) + { + fileInfo.Refresh(); + + if (!fileInfo.Exists) + throw new ArgumentException($"Input file {fileInfo.FullName} does not exist!"); + + _file = fileInfo; + + new FFProbe().ParseVideoInfo(this); + } + + /// + /// Create a video information object from a target path. + /// + /// Path to video. + public VideoInfo(string path) : this(new FileInfo(path)) + { + } + + /// + /// Duration of the video file. + /// + public TimeSpan Duration { get; internal set; } + + /// + /// Audio format of the video file. + /// + public string AudioFormat { get; internal set; } + + /// + /// Video format of the video file. + /// + public string VideoFormat { get; internal set; } + + /// + /// Aspect ratio. + /// + public string Ratio { get; internal set; } + + /// + /// Video frame rate. + /// + public double FrameRate { get; internal set; } + + /// + /// Height of the video file. + /// + public int Height { get; internal set; } + + /// + /// Width of the video file. + /// + public int Width { get; internal set; } + + /// + /// Video file size in MegaBytes (MB). + /// + public double Size { get; internal set; } + + /// + /// Gets the name of the file. + /// + public string Name => _file.Name; + + /// + /// Gets the full path of the file. + /// + public string FullName => _file.FullName; + + /// + /// Gets the file extension. + /// + public string Extension => _file.Extension; + + /// + /// Gets a flag indicating if the file is read-only. + /// + public bool IsReadOnly => _file.IsReadOnly; + + /// + /// Gets a flag indicating if the file exists (no cache, per call verification). + /// + public bool Exists => File.Exists(FullName); + + /// + /// Gets the creation date. + /// + public DateTime CreationTime => _file.CreationTime; + + /// + /// Gets the parent directory information. + /// + public DirectoryInfo Directory => _file.Directory; + + /// + /// Create a video information object from a file information object. + /// + /// Video file information. + /// + public static VideoInfo FromFileInfo(FileInfo fileInfo) + { + return FromPath(fileInfo.FullName); + } + + /// + /// Create a video information object from a target path. + /// + /// Path to video. + /// + public static VideoInfo FromPath(string path) + { + return new VideoInfo(path); + } + + /// + /// Pretty prints the video information. + /// + /// + public override string ToString() + { + return "Video Path : " + FullName + Environment.NewLine + + "Video Root : " + Directory.FullName + Environment.NewLine + + "Video Name: " + Name + Environment.NewLine + + "Video Extension : " + Extension + Environment.NewLine + + "Video Duration : " + Duration + Environment.NewLine + + "Audio Format : " + AudioFormat + Environment.NewLine + + "Video Format : " + VideoFormat + Environment.NewLine + + "Aspect Ratio : " + Ratio + Environment.NewLine + + "Framerate : " + FrameRate + "fps" + Environment.NewLine + + "Resolution : " + Width + "x" + Height + Environment.NewLine + + "Size : " + Size + " MB"; + } + + /// + /// Open a file stream. + /// + /// Opens a file in a specified mode. + /// File stream of the video file. + public FileStream FileOpen(FileMode mode) + { + return _file.Open(mode); + } + + /// + /// Move file to a specific directory. + /// + /// + public void MoveTo(DirectoryInfo destination) + { + var newLocation = $"{destination.FullName}\\{Name}{Extension}"; + _file.MoveTo(newLocation); + _file = new FileInfo(newLocation); + } + + /// + /// Delete the file. + /// + public void Delete() + { + _file.Delete(); + } + + /// + /// Converts video info to file info. + /// + /// FileInfo + public FileInfo ToFileInfo() + { + return new FileInfo(_file.FullName); + } + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..58b322d --- /dev/null +++ b/README.md @@ -0,0 +1,333 @@ +![FFMpeg Sharp](https://media.licdn.com/media/gcrc/dms/image/C5612AQFDCKxnyQ3tmw/article-cover_image-shrink_600_2000/0?e=1542844800&v=beta&t=ntfxKUaio7wjO2VFRL4o7gyoIPNKT95SPt94etMFuzw) + +# FFMpegCore [![NuGet Badge](https://buildstats.info/nuget/FFMpegCore)](https://www.nuget.org/packages/FFMpegCore/) + +## Setup + +#### NuGet: + +``` +Install-Package FFMpegCore +``` + +A great way to use FFMpeg encoding when writing video applications, client-side and server-side. It has wrapper methods that allow conversion to all web formats: MP4, OGV, TS and methods of capturing screens from the videos. + +### FFProbe + +FFProbe is used to gather video information +```csharp +static void Main(string[] args) +{ + string inputFile = "G:\\input.mp4"; + + // loaded from configuration + var video = new VideoInfo(inputFile); + + string output = video.ToString(); + + Console.WriteLine(output); +} +``` + +Sample output: +```csharp +Video Path : G:\input.mp4 +Video Root : G:\\ +Video Name: input.mp4 +Video Extension : .mp4 +Video Duration : 00:00:09 +Audio Format : none +Video Format : h264 +Aspect Ratio : 16:9 +Framerate : 30fps +Resolution : 1280x720 +Size : 2.88 Mb +``` + +### FFMpeg +Convert your video files to web ready formats: + +```csharp +static void Main(string[] args) +{ + string inputFile = "input_path_goes_here"; + var encoder = new FFMpeg(); + FileInfo outputFile = new FileInfo("output_path_goes_here"); + + var video = VideoInfo.FromPath(inputFile); + + // easily track conversion progress + encoder.OnProgress += (percentage) => Console.WriteLine("Progress {0}%", percentage); + + // MP4 conversion + encoder.Convert( + video, + outputFile, + VideoType.Mp4, + Speed.UltraFast, + VideoSize.Original, + AudioQuality.Hd, + true + ); + // OGV conversion + encoder.Convert( + video, + outputFile, + VideoType.Ogv, + Speed.UltraFast, + VideoSize.Original, + AudioQuality.Hd, + true + ); + // TS conversion + encoder.Convert( + video, + outputFile, + VideoType.Ts + ); +} +``` + +Easily capture screens from your videos: +```csharp +static void Main(string[] args) +{ + string inputFile = "input_path_goes_here"; + FileInfo output = new FileInfo("output_path_goes_here"); + + var video = VideoInfo.FromPath(inputFile); + + new FFMpeg() + .Snapshot( + video, + output, + new Size(200, 400), + TimeSpan.FromMinutes(1) + ); +} +``` + +Join video parts: +```csharp +static void Main(string[] args) +{ + FFMpeg encoder = new FFMpeg(); + + encoder.Join( + new FileInfo(@"..\joined_video.mp4"), + VideoInfo.FromPath(@"..\part1.mp4"), + VideoInfo.FromPath(@"..\part2.mp4"), + VideoInfo.FromPath(@"..\part3.mp4") + ); +} +``` + +Join image sequences: +```csharp +static void Main(string[] args) +{ + FFMpeg encoder = new FFMpeg(); + + encoder.JoinImageSequence( + new FileInfo(@"..\joined_video.mp4"), + 1, // FPS + ImageInfo.FromPath(@"..\1.png"), + ImageInfo.FromPath(@"..\2.png"), + ImageInfo.FromPath(@"..\3.png") + ); +} +``` + +Strip audio track from videos: +```csharp +static void Main(string[] args) +{ + string inputFile = "input_path_goes_here", + outputFile = "output_path_goes_here"; + + new FFMpeg() + .Mute( + VideoInfo.FromPath(inputFile), + new FileInfo(outputFile) + ); +} +``` + +Save audio track from video: +```csharp +static void Main(string[] args) +{ + string inputVideoFile = "input_path_goes_here", + outputAudioFile = "output_path_goes_here"; + + new FFMpeg() + .ExtractAudio( + VideoInfo.FromPath(inputVideoFile), + new FileInfo(outputAudioFile) + ); +} +``` + +Add audio track to video: +```csharp +static void Main(string[] args) +{ + string inputVideoFile = "input_path_goes_here", + inputAudioFile = "input_path_goes_here", + outputVideoFile = "output_path_goes_here"; + + FFMpeg encoder = new FFMpeg(); + + new FFMpeg() + .ReplaceAudio( + VideoInfo.FromPath(inputVideoFile), + new FileInfo(inputAudioFile), + new FileInfo(outputVideoFile) + ); +} +``` + +Add poster image to audio file (good for youtube videos): +```csharp +static void Main(string[] args) +{ + string inputImageFile = "input_path_goes_here", + inputAudioFile = "input_path_goes_here", + outputVideoFile = "output_path_goes_here"; + + FFMpeg encoder = new FFMpeg(); + + ((Bitmap)Image.FromFile(inputImageFile)) + .AddAudio( + new FileInfo(inputAudioFile), + new FileInfo(outputVideoFile) + ); + + /* OR */ + + new FFMpeg() + .PosterWithAudio( + inputImageFile, + new FileInfo(inputAudioFile), + new FileInfo(outputVideoFile) + ); +} +``` + +Control over the 'FFmpeg' process doing the job: +```csharp +static void Main(string[] args) +{ + string inputVideoFile = "input_path_goes_here", + outputVideoFile = "input_path_goes_here"; + + FFMpeg encoder = new FFMpeg(); + + // start the conversion process + Task.Run(() => { + encoder.Convert(new VideoInfo(inputVideoFile), new FileInfo(outputVideoFile)); + }); + + // stop encoding after 2 seconds (only for example purposes) + Thread.Sleep(2000); + encoder.Stop(); +} +``` +### Enums + +Video Size enumeration: + +```csharp +public enum VideoSize +{ + HD, + FullHD, + ED, + LD, + Original +} +``` + +Speed enumeration: + +```csharp +public enum Speed +{ + VerySlow, + Slower, + Slow, + Medium, + Fast, + Faster, + VeryFast, + SuperFast, + UltraFast +} +``` +Audio codecs enumeration: + +```csharp +public enum AudioCodec +{ + Aac, + LibVorbis +} +``` + +Audio quality presets enumeration: + +```csharp +public enum AudioQuality +{ + Ultra = 384, + Hd = 192, + Normal = 128, + Low = 64 +} +``` + +Video codecs enumeration: + +```csharp +public enum VideoCodec +{ + LibX264, + LibVpx, + LibTheora, + Png, + MpegTs +} +``` +### ArgumentBuilder +Custom video converting presets could be created with help of `ArgumentsContainer` class: +```csharp +var container = new ArgumentsContainer(); +container.Add(new VideoCodecArgument(VideoCodec.LibX264)); +container.Add(new ScaleArgument(VideoSize.Hd)); + +var ffmpeg = new FFMpeg(); +var result = ffmpeg.Convert(container, new FileInfo("input.mp4"), new FileInfo("output.mp4")); +``` + +Other availible arguments could be found in `FFMpegCore.FFMPEG.Arguments` namespace. + +If you need to create your custom argument, you just need to create new class, that is inherited from `Argument`, `Argument` or `Argument` +For example: +```csharp +public class OverrideArgument : Argument +{ + public override string GetStringValue() + { + return "-y"; + } +} +``` +## Contributors + + + + +### License + +Copyright © 2018, [Vlad Jerca](https://github.com/vladjerca). +Released under the [MIT license](https://github.com/jonschlinkert/github-contributors/blob/master/LICENSE). diff --git a/pack.ps1 b/pack.ps1 new file mode 100644 index 0000000..b395d74 --- /dev/null +++ b/pack.ps1 @@ -0,0 +1 @@ +.\.nuget\nuget.exe pack .\FFMpegCore\ -Prop Configuration=Release \ No newline at end of file