diff --git a/FFMpegCore.Extensions.Downloader/Enums/FFMpegBinaries.cs b/FFMpegCore.Extensions.Downloader/Enums/FFMpegBinaries.cs new file mode 100644 index 00000000..d92116a4 --- /dev/null +++ b/FFMpegCore.Extensions.Downloader/Enums/FFMpegBinaries.cs @@ -0,0 +1,9 @@ +namespace FFMpegCore.Extensions.Downloader.Enums; + +[Flags] +public enum FFMpegBinaries : ushort +{ + FFMpeg, + FFProbe, + FFPlay +} diff --git a/FFMpegCore.Extensions.Downloader/Enums/FFMpegVersions.cs b/FFMpegCore.Extensions.Downloader/Enums/FFMpegVersions.cs new file mode 100644 index 00000000..a2e5f097 --- /dev/null +++ b/FFMpegCore.Extensions.Downloader/Enums/FFMpegVersions.cs @@ -0,0 +1,39 @@ +using System.ComponentModel; + +namespace FFMpegCore.Extensions.Downloader.Enums; + +public enum FFMpegVersions : ushort +{ + [Description("https://ffbinaries.com/api/v1/version/latest")] + Latest, + + [Description("https://ffbinaries.com/api/v1/version/6.1")] + V6_1, + + [Description("https://ffbinaries.com/api/v1/version/5.1")] + V5_1, + + [Description("https://ffbinaries.com/api/v1/version/4.4.1")] + V4_4_1, + + [Description("https://ffbinaries.com/api/v1/version/4.2.1")] + V4_2_1, + + [Description("https://ffbinaries.com/api/v1/version/4.2")] + V4_2, + + [Description("https://ffbinaries.com/api/v1/version/4.1")] + V4_1, + + [Description("https://ffbinaries.com/api/v1/version/4.0")] + V4_0, + + [Description("https://ffbinaries.com/api/v1/version/3.4")] + V3_4, + + [Description("https://ffbinaries.com/api/v1/version/3.3")] + V3_3, + + [Description("https://ffbinaries.com/api/v1/version/3.2")] + V3_2 +} diff --git a/FFMpegCore.Extensions.Downloader/Enums/SupportedPlatforms.cs b/FFMpegCore.Extensions.Downloader/Enums/SupportedPlatforms.cs new file mode 100644 index 00000000..0378f3e0 --- /dev/null +++ b/FFMpegCore.Extensions.Downloader/Enums/SupportedPlatforms.cs @@ -0,0 +1,13 @@ +namespace FFMpegCore.Extensions.Downloader.Enums; + +public enum SupportedPlatforms : ushort +{ + Windows64, + Windows32, + Linux64, + Linux32, + LinuxArmhf, + LinuxArmel, + LinuxArm64, + Osx64 +} diff --git a/FFMpegCore.Extensions.Downloader/Exceptions/FFMpegDownloaderException.cs b/FFMpegCore.Extensions.Downloader/Exceptions/FFMpegDownloaderException.cs new file mode 100644 index 00000000..c7d2ead9 --- /dev/null +++ b/FFMpegCore.Extensions.Downloader/Exceptions/FFMpegDownloaderException.cs @@ -0,0 +1,18 @@ +namespace FFMpegCore.Extensions.Downloader.Exceptions; + +/// +/// Custom exception for FFMpegDownloader +/// +public class FFMpegDownloaderException : Exception +{ + public string Detail { get; set; } = ""; + + public FFMpegDownloaderException(string message) : base(message) + { + } + + public FFMpegDownloaderException(string message, string detail) : base(message) + { + Detail = detail; + } +} diff --git a/FFMpegCore.Extensions.Downloader/Extensions/EnumExtensions.cs b/FFMpegCore.Extensions.Downloader/Extensions/EnumExtensions.cs new file mode 100644 index 00000000..7930c130 --- /dev/null +++ b/FFMpegCore.Extensions.Downloader/Extensions/EnumExtensions.cs @@ -0,0 +1,22 @@ +using System.ComponentModel; + +namespace FFMpegCore.Extensions.Downloader.Extensions; + +internal static class EnumExtensions +{ + public static string GetDescription(this Enum enumValue) + { + var field = enumValue.GetType().GetField(enumValue.ToString()); + if (field == null) + { + return enumValue.ToString(); + } + + if (Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) is DescriptionAttribute attribute) + { + return attribute.Description; + } + + return enumValue.ToString(); + } +} diff --git a/FFMpegCore.Extensions.Downloader/FFMpegCore.Extensions.Downloader.csproj b/FFMpegCore.Extensions.Downloader/FFMpegCore.Extensions.Downloader.csproj new file mode 100644 index 00000000..0c6f791e --- /dev/null +++ b/FFMpegCore.Extensions.Downloader/FFMpegCore.Extensions.Downloader.csproj @@ -0,0 +1,23 @@ + + + + netstandard2.1 + enable + + + + true + FFMpeg downloader extension for FFMpegCore + 5.0.0 + ../nupkg + + + ffmpeg ffprobe convert video audio mediafile resize analyze download + Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev, Kerry Cao + + + + + + + diff --git a/FFMpegCore.Extensions.Downloader/FFMpegDownloader.cs b/FFMpegCore.Extensions.Downloader/FFMpegDownloader.cs new file mode 100644 index 00000000..0def62ba --- /dev/null +++ b/FFMpegCore.Extensions.Downloader/FFMpegDownloader.cs @@ -0,0 +1,53 @@ +using FFMpegCore.Extensions.Downloader.Enums; +using FFMpegCore.Extensions.Downloader.Exceptions; +using FFMpegCore.Extensions.Downloader.Services; + +namespace FFMpegCore.Extensions.Downloader; + +public class FFMpegDownloader +{ + /// + /// Download the latest FFMpeg suite binaries for current platform + /// + /// used to explicitly state the version of binary you want to download + /// used to explicitly state the binaries you want to download (ffmpeg, ffprobe, ffplay) + /// used to explicitly state the os and architecture you want to download + /// a list of the binaries that have been successfully downloaded + public static async Task> DownloadFFMpegSuite( + FFMpegVersions version = FFMpegVersions.Latest, + FFMpegBinaries binaries = FFMpegBinaries.FFMpeg | FFMpegBinaries.FFProbe, + SupportedPlatforms? platformOverride = null) + { + // get all available versions + var versionInfo = await FFbinariesService.GetVersionInfo(version); + + // get the download info for the current platform + var downloadInfo = versionInfo.BinaryInfo?.GetCompatibleDownloadInfo(platformOverride) ?? + throw new FFMpegDownloaderException("Failed to get compatible download info"); + + var successList = new List(); + + // download ffmpeg if selected + if (binaries.HasFlag(FFMpegBinaries.FFMpeg) && downloadInfo.FFMpeg is not null) + { + await using var zipStream = await FFbinariesService.DownloadFileAsSteam(new Uri(downloadInfo.FFMpeg)); + successList.AddRange(FFbinariesService.ExtractZipAndSave(zipStream)); + } + + // download ffprobe if selected + if (binaries.HasFlag(FFMpegBinaries.FFProbe) && downloadInfo.FFProbe is not null) + { + await using var zipStream = await FFbinariesService.DownloadFileAsSteam(new Uri(downloadInfo.FFProbe)); + successList.AddRange(FFbinariesService.ExtractZipAndSave(zipStream)); + } + + // download ffplay if selected + if (binaries.HasFlag(FFMpegBinaries.FFPlay) && downloadInfo.FFPlay is not null) + { + await using var zipStream = await FFbinariesService.DownloadFileAsSteam(new Uri(downloadInfo.FFPlay)); + successList.AddRange(FFbinariesService.ExtractZipAndSave(zipStream)); + } + + return successList; + } +} diff --git a/FFMpegCore.Extensions.Downloader/Models/BinaryInfo.cs b/FFMpegCore.Extensions.Downloader/Models/BinaryInfo.cs new file mode 100644 index 00000000..e91fcee2 --- /dev/null +++ b/FFMpegCore.Extensions.Downloader/Models/BinaryInfo.cs @@ -0,0 +1,74 @@ +using System.Runtime.InteropServices; +using System.Text.Json.Serialization; +using FFMpegCore.Extensions.Downloader.Enums; + +namespace FFMpegCore.Extensions.Downloader.Models; + +internal record BinaryInfo +{ + [JsonPropertyName("windows-64")] public DownloadInfo? Windows64 { get; set; } + + [JsonPropertyName("windows-32")] public DownloadInfo? Windows32 { get; set; } + + [JsonPropertyName("linux-32")] public DownloadInfo? Linux32 { get; set; } + + [JsonPropertyName("linux-64")] public DownloadInfo? Linux64 { get; set; } + + [JsonPropertyName("linux-armhf")] public DownloadInfo? LinuxArmhf { get; set; } + + [JsonPropertyName("linux-armel")] public DownloadInfo? LinuxArmel { get; set; } + + [JsonPropertyName("linux-arm64")] public DownloadInfo? LinuxArm64 { get; set; } + + [JsonPropertyName("osx-64")] public DownloadInfo? Osx64 { get; set; } + + /// + /// Automatically get the compatible download info for current os and architecture + /// + /// + /// + /// + /// + public DownloadInfo? GetCompatibleDownloadInfo(SupportedPlatforms? platformOverride = null) + { + if (platformOverride is not null) + { + return platformOverride switch + { + SupportedPlatforms.Windows64 => Windows64, + SupportedPlatforms.Windows32 => Windows32, + SupportedPlatforms.Linux64 => Linux64, + SupportedPlatforms.Linux32 => Linux32, + SupportedPlatforms.LinuxArmhf => LinuxArmhf, + SupportedPlatforms.LinuxArmel => LinuxArmel, + SupportedPlatforms.LinuxArm64 => LinuxArm64, + SupportedPlatforms.Osx64 => Osx64, + _ => throw new ArgumentOutOfRangeException(nameof(platformOverride), platformOverride, null) + }; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return RuntimeInformation.OSArchitecture == Architecture.X64 ? Windows64 : Windows32; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return RuntimeInformation.OSArchitecture switch + { + Architecture.X86 => Linux32, + Architecture.X64 => Linux64, + Architecture.Arm => LinuxArmhf, + Architecture.Arm64 => LinuxArm64, + _ => LinuxArmel + }; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return Osx64; + } + + throw new PlatformNotSupportedException("Unsupported OS or Architecture"); + } +} diff --git a/FFMpegCore.Extensions.Downloader/Models/DownloadInfo.cs b/FFMpegCore.Extensions.Downloader/Models/DownloadInfo.cs new file mode 100644 index 00000000..5b7c0fdc --- /dev/null +++ b/FFMpegCore.Extensions.Downloader/Models/DownloadInfo.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace FFMpegCore.Extensions.Downloader.Models; + +internal record DownloadInfo +{ + [JsonPropertyName("ffmpeg")] public string? FFMpeg { get; set; } + + [JsonPropertyName("ffprobe")] public string? FFProbe { get; set; } + + [JsonPropertyName("ffplay")] public string? FFPlay { get; set; } +} diff --git a/FFMpegCore.Extensions.Downloader/Models/VersionInfo.cs b/FFMpegCore.Extensions.Downloader/Models/VersionInfo.cs new file mode 100644 index 00000000..ef24f629 --- /dev/null +++ b/FFMpegCore.Extensions.Downloader/Models/VersionInfo.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace FFMpegCore.Extensions.Downloader.Models; + +internal record VersionInfo +{ + [JsonPropertyName("version")] public string? Version { get; set; } + + [JsonPropertyName("permalink")] public string? Permalink { get; set; } + + [JsonPropertyName("bin")] public BinaryInfo? BinaryInfo { get; set; } +} diff --git a/FFMpegCore.Extensions.Downloader/Services/FFbinariesService.cs b/FFMpegCore.Extensions.Downloader/Services/FFbinariesService.cs new file mode 100644 index 00000000..1b7edc27 --- /dev/null +++ b/FFMpegCore.Extensions.Downloader/Services/FFbinariesService.cs @@ -0,0 +1,71 @@ +using System.IO.Compression; +using System.Text.Json; +using FFMpegCore.Extensions.Downloader.Enums; +using FFMpegCore.Extensions.Downloader.Exceptions; +using FFMpegCore.Extensions.Downloader.Extensions; +using FFMpegCore.Extensions.Downloader.Models; + +namespace FFMpegCore.Extensions.Downloader.Services; + +/// +/// Service to interact with ffbinaries.com API +/// +internal class FFbinariesService +{ + /// + /// Get version info from ffbinaries.com + /// + /// use to explicitly state the version of ffmpeg you want + /// + /// + internal static async Task GetVersionInfo(FFMpegVersions version) + { + var versionUri = version.GetDescription(); + + HttpClient client = new(); + var response = await client.GetAsync(versionUri); + + if (!response.IsSuccessStatusCode) + { + throw new FFMpegDownloaderException($"Failed to get version info from {versionUri}", "network error"); + } + + var jsonString = await response.Content.ReadAsStringAsync(); + var versionInfo = JsonSerializer.Deserialize(jsonString); + + return versionInfo ?? + throw new FFMpegDownloaderException($"Failed to deserialize version info from {versionUri}", jsonString); + } + + /// + /// Download file from uri + /// + /// uri of the file + /// + internal static async Task DownloadFileAsSteam(Uri address) + { + var client = new HttpClient(); + return await client.GetStreamAsync(address); + } + + /// + /// Extracts the binaries from the zip stream and saves them to the current binary folder + /// + /// steam of the zip file + /// + internal static IEnumerable ExtractZipAndSave(Stream zipStream) + { + using var archive = new ZipArchive(zipStream, ZipArchiveMode.Read); + List files = new(); + foreach (var entry in archive.Entries) + { + if (entry.Name is "ffmpeg" or "ffmpeg.exe" or "ffprobe.exe" or "ffprobe" or "ffplay.exe" or "ffplay") + { + entry.ExtractToFile(Path.Combine(GlobalFFOptions.Current.BinaryFolder, entry.Name), true); + files.Add(Path.Combine(GlobalFFOptions.Current.BinaryFolder, entry.Name)); + } + } + + return files; + } +} diff --git a/FFMpegCore.Test/DownloaderTests.cs b/FFMpegCore.Test/DownloaderTests.cs new file mode 100644 index 00000000..f2eb5633 --- /dev/null +++ b/FFMpegCore.Test/DownloaderTests.cs @@ -0,0 +1,23 @@ +using FFMpegCore.Extensions.Downloader; +using FFMpegCore.Extensions.Downloader.Enums; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace FFMpegCore.Test; + +[TestClass] +public class DownloaderTests +{ + [TestMethod] + public void GetSpecificVersionTest() + { + var binaries = FFMpegDownloader.DownloadFFMpegSuite(FFMpegVersions.V6_1).Result; + Assert.IsTrue(binaries.Count == 2); + } + + [TestMethod] + public void GetAllLatestSuiteTest() + { + var binaries = FFMpegDownloader.DownloadFFMpegSuite().Result; + Assert.IsTrue(binaries.Count == 2); // many platforms have only ffmpeg and ffprobe + } +} diff --git a/FFMpegCore.Test/FFMpegCore.Test.csproj b/FFMpegCore.Test/FFMpegCore.Test.csproj index 77914234..9fd30b24 100644 --- a/FFMpegCore.Test/FFMpegCore.Test.csproj +++ b/FFMpegCore.Test/FFMpegCore.Test.csproj @@ -24,6 +24,7 @@ + diff --git a/FFMpegCore.sln b/FFMpegCore.sln index 7ab09297..b99a44ea 100644 --- a/FFMpegCore.sln +++ b/FFMpegCore.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.31005.135 +# Visual Studio Version 17 +VisualStudioVersion = 17.7.34003.232 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMpegCore", "FFMpegCore\FFMpegCore.csproj", "{19DE2EC2-9955-4712-8096-C22EF6713E4F}" EndProject @@ -13,6 +13,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMpegCore.Extensions.Syste EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFMpegCore.Extensions.SkiaSharp", "FFMpegCore.Extensions.SkiaSharp\FFMpegCore.Extensions.SkiaSharp.csproj", "{5A76F9B7-3681-4551-A9B6-8D3AC5DA1090}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FFMpegCore.Extensions.Downloader", "FFMpegCore.Extensions.Downloader\FFMpegCore.Extensions.Downloader.csproj", "{5FA30158-CAB0-44FD-AD98-C31F5E3D5A56}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,6 +41,10 @@ Global {5A76F9B7-3681-4551-A9B6-8D3AC5DA1090}.Debug|Any CPU.Build.0 = Debug|Any CPU {5A76F9B7-3681-4551-A9B6-8D3AC5DA1090}.Release|Any CPU.ActiveCfg = Release|Any CPU {5A76F9B7-3681-4551-A9B6-8D3AC5DA1090}.Release|Any CPU.Build.0 = Release|Any CPU + {5FA30158-CAB0-44FD-AD98-C31F5E3D5A56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5FA30158-CAB0-44FD-AD98-C31F5E3D5A56}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5FA30158-CAB0-44FD-AD98-C31F5E3D5A56}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5FA30158-CAB0-44FD-AD98-C31F5E3D5A56}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/README.md b/README.md index 33f7ddfb..716d85fa 100644 --- a/README.md +++ b/README.md @@ -162,7 +162,12 @@ If you want to use `System.Drawing.Bitmap`s as `IVideoFrame`s, a `BitmapVideoFra # Binaries -## Installation +## Runtime Auto Installation +You can install a version of ffmpeg suite at runtime using `FFMpegDownloader.DownloadFFMpegSuite();` + +This feature uses the api from [ffbinaries](https://ffbinaries.com/api). + +## Manual Installation If you prefer to manually download them, visit [ffbinaries](https://ffbinaries.com/downloads) or [zeranoe Windows builds](https://ffmpeg.zeranoe.com/builds/). ### Windows (using choco)