From dfc486db1d8b0a796dcda1319676e1fb98062753 Mon Sep 17 00:00:00 2001 From: Kerry Cao Date: Wed, 6 Sep 2023 13:42:22 -0600 Subject: [PATCH] ffmpeg downloader moved to seperate package --- .../FFMpegCore.Downloader.csproj | 23 ++ FFMpegCore.Downloader/FFMpegDownloader.cs | 272 ++++++++++++++++++ FFMpegCore.Test/FFMpegCore.Test.csproj | 1 + FFMpegCore.sln | 10 +- 4 files changed, 304 insertions(+), 2 deletions(-) create mode 100644 FFMpegCore.Downloader/FFMpegCore.Downloader.csproj create mode 100644 FFMpegCore.Downloader/FFMpegDownloader.cs diff --git a/FFMpegCore.Downloader/FFMpegCore.Downloader.csproj b/FFMpegCore.Downloader/FFMpegCore.Downloader.csproj new file mode 100644 index 0000000..720cbf1 --- /dev/null +++ b/FFMpegCore.Downloader/FFMpegCore.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.Downloader/FFMpegDownloader.cs b/FFMpegCore.Downloader/FFMpegDownloader.cs new file mode 100644 index 0000000..36f27c5 --- /dev/null +++ b/FFMpegCore.Downloader/FFMpegDownloader.cs @@ -0,0 +1,272 @@ +using System.IO.Compression; +using System.Net; +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace FFMpegCore.Downloader; + +/// +/// Downloads the latest FFMpeg suite binaries from ffbinaries.com. +/// +public class FFMpegDownloader +{ + [Flags] + public enum FFMpegBinaries : ushort + { + FFMpeg, + FFProbe, + FFPlay + } + + public enum FFMpegVersions : ushort + { + Latest, + V4_4_1, + V4_2_1, + V4_2, + V4_1, + V4_0, + V3_4, + V3_3, + V3_2 + } + + public enum PlatformOverride : short + { + Windows64, + Windows32, + Linux64, + Linux32, + LinuxArmhf, + LinuxArmel, + LinuxArm64, + Osx64 + } + + /// + /// 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, + PlatformOverride? platformOverride = null) + { + var versionInfo = await GetVersionInfo(version); + var downloadInfo = versionInfo.BinaryInfo?.GetCompatibleDownloadInfo(platformOverride) ?? + throw new FFMpegDownloaderException("Failed to get compatible download info"); + + var successList = new List(); + + // if ffmpeg is selected + if (binaries.HasFlag(FFMpegBinaries.FFMpeg) && downloadInfo.FFMpeg is not null) + { + var zipStream = DownloadFileAsSteam(new Uri(downloadInfo.FFMpeg)); + successList.AddRange(ExtractZipAndSave(zipStream)); + } + + // if ffprobe is selected + if (binaries.HasFlag(FFMpegBinaries.FFProbe) && downloadInfo.FFProbe is not null) + { + var zipStream = DownloadFileAsSteam(new Uri(downloadInfo.FFProbe)); + successList.AddRange(ExtractZipAndSave(zipStream)); + } + + // if ffplay is selected + if (binaries.HasFlag(FFMpegBinaries.FFPlay) && downloadInfo.FFPlay is not null) + { + var zipStream = DownloadFileAsSteam(new Uri(downloadInfo.FFPlay)); + successList.AddRange(ExtractZipAndSave(zipStream)); + } + + return successList; + } + + /// + /// Download file from uri + /// + /// uri of the file + /// + private static MemoryStream DownloadFileAsSteam(Uri address) + { + var client = new WebClient(); + var fileStream = new MemoryStream(client.DownloadData(address)); + fileStream.Position = 0; + + return fileStream; + } + + /// + /// Extracts the binaries from the zip stream and saves them to the current binary folder + /// + /// steam of the zip file + /// + private 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; + } + + #region FFbinaries api + + private class DownloadInfo + { + [JsonPropertyName("ffmpeg")] public string? FFMpeg { get; set; } + + [JsonPropertyName("ffprobe")] public string? FFProbe { get; set; } + + [JsonPropertyName("ffplay")] public string? FFPlay { get; set; } + } + + private class 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(PlatformOverride? platformOverride = null) + { + if (platformOverride is not null) + { + return platformOverride switch + { + PlatformOverride.Windows64 => Windows64, + PlatformOverride.Windows32 => Windows32, + PlatformOverride.Linux64 => Linux64, + PlatformOverride.Linux32 => Linux32, + PlatformOverride.LinuxArmhf => LinuxArmhf, + PlatformOverride.LinuxArmel => LinuxArmel, + PlatformOverride.LinuxArm64 => LinuxArm64, + PlatformOverride.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"); + } + } + + private class VersionInfo + { + [JsonPropertyName("version")] public string? Version { get; set; } + + [JsonPropertyName("permalink")] public string? Permalink { get; set; } + + [JsonPropertyName("bin")] public BinaryInfo? BinaryInfo { get; set; } + } + + private static readonly Dictionary _FFBinariesAPIs = new() + { + { FFMpegVersions.Latest, "https://ffbinaries.com/api/v1/version/latest" }, + { FFMpegVersions.V4_4_1, "https://ffbinaries.com/api/v1/version/4.4.1" }, + { FFMpegVersions.V4_2_1, "https://ffbinaries.com/api/v1/version/4.2.1" }, + { FFMpegVersions.V4_2, "https://ffbinaries.com/api/v1/version/4.2" }, + { FFMpegVersions.V4_1, "https://ffbinaries.com/api/v1/version/4.1" }, + { FFMpegVersions.V4_0, "https://ffbinaries.com/api/v1/version/4.0" }, + { FFMpegVersions.V3_4, "https://ffbinaries.com/api/v1/version/3.4" }, + { FFMpegVersions.V3_3, "https://ffbinaries.com/api/v1/version/3.3" }, + { FFMpegVersions.V3_2, "https://ffbinaries.com/api/v1/version/3.2" } + }; + + /// + /// Get version info from ffbinaries.com + /// + /// use to explicitly state the version of ffmpeg you want + /// + /// + private static async Task GetVersionInfo(FFMpegVersions version) + { + if (!_FFBinariesAPIs.TryGetValue(version, out var versionUri)) + { + throw new FFMpegDownloaderException($"Invalid version selected: {version}", "contact dev"); + } + + 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); + } + + #endregion +} + +/// +/// Custom exception for FFMpegDownloader +/// +public class FFMpegDownloaderException : Exception +{ + public FFMpegDownloaderException(string message) : base(message) + { + } + + public FFMpegDownloaderException(string message, string detail) : base(message) + { + Detail = detail; + } + + public string Detail { get; set; } = ""; +} diff --git a/FFMpegCore.Test/FFMpegCore.Test.csproj b/FFMpegCore.Test/FFMpegCore.Test.csproj index b78af1b..d54d269 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 7ab0929..f7f42d2 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.Downloader", "FFMpegCore.Downloader\FFMpegCore.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