From da7d1fafed127d1f4c89f39d191e2a87f7e756b5 Mon Sep 17 00:00:00 2001 From: Kerry Cao Date: Wed, 6 Sep 2023 01:25:51 -0600 Subject: [PATCH] support added for linux, macos win32, win64, lnx32, lnx64, lnx-armhf, lnx-armel, lnx-arm64, osx64 --- FFMpegCore.Test/DownloaderTests.cs | 61 +----- FFMpegCore/Helpers/FFMpegDownloader.cs | 285 +++++++++++++++++-------- 2 files changed, 209 insertions(+), 137 deletions(-) diff --git a/FFMpegCore.Test/DownloaderTests.cs b/FFMpegCore.Test/DownloaderTests.cs index 2d968ad..c677d5d 100644 --- a/FFMpegCore.Test/DownloaderTests.cs +++ b/FFMpegCore.Test/DownloaderTests.cs @@ -1,5 +1,4 @@ -using System.Runtime.InteropServices; -using FFMpegCore.Helpers; +using FFMpegCore.Helpers; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace FFMpegCore.Test; @@ -8,58 +7,18 @@ namespace FFMpegCore.Test; public class DownloaderTests { [TestMethod] - public void GetLatestSuiteTest() + public void GetAllLatestSuiteTest() { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - var fileNames = FFMpegDownloader.AutoDownloadFFMpegSuite(); - Assert.IsTrue(fileNames.Count == 3); - } - else - { - Assert.Inconclusive("This test is only for Windows"); - } + var binaries = FFMpegDownloader.DownloadFFMpegSuite(binaries: FFMpegDownloader.FFMpegBinaries.FFProbe | + FFMpegDownloader.FFMpegBinaries.FFMpeg | + FFMpegDownloader.FFMpegBinaries.FFPlay).Result; + Assert.IsTrue(binaries.Count >= 2); // many platforms have only ffmpeg and ffprobe } - + [TestMethod] - public void GetLatestFFMpegTest() + public void GetSpecificVersionTest() { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - var fileNames = FFMpegDownloader.AutoDownloadFFMpeg(); - Assert.IsTrue(fileNames.Count == 1); - } - else - { - Assert.Inconclusive("This test is only for Windows"); - } - } - - [TestMethod] - public void GetLatestFFProbeTest() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - var fileNames = FFMpegDownloader.AutoDownloadFFProbe(); - Assert.IsTrue(fileNames.Count == 1); - } - else - { - Assert.Inconclusive("This test is only for Windows"); - } - } - - [TestMethod] - public void GetLatestFFPlayTest() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - var fileNames = FFMpegDownloader.AutoDownloadFFPlay(); - Assert.IsTrue(fileNames.Count == 1); - } - else - { - Assert.Inconclusive("This test is only for Windows"); - } + var binaries = FFMpegDownloader.DownloadFFMpegSuite(FFMpegDownloader.FFMpegVersions.V4_0).Result; + Assert.IsTrue(binaries.Count == 2); } } diff --git a/FFMpegCore/Helpers/FFMpegDownloader.cs b/FFMpegCore/Helpers/FFMpegDownloader.cs index d6149e8..c07a30e 100644 --- a/FFMpegCore/Helpers/FFMpegDownloader.cs +++ b/FFMpegCore/Helpers/FFMpegDownloader.cs @@ -1,43 +1,38 @@ -using System.ComponentModel; +using System.IO.Compression; using System.Net; -using System.IO; -using System.IO.Compression; - +using System.Runtime.InteropServices; +using System.Text.Json; +using System.Text.Json.Serialization; namespace FFMpegCore.Helpers; -using System.Runtime.InteropServices; /// -/// Downloads the latest FFMpeg suite binaries from GitHub. Only supported for windows at the moment. +/// Downloads the latest FFMpeg suite binaries from ffbinaries.com. /// -public class FFMpegDownloader // this class is built to be easily modified to support other platforms +public class FFMpegDownloader { - private static Dictionary Windows64FFMpegDownloadUrls = new() + [Flags] + public enum FFMpegBinaries : ushort { - { FFMpegVersions.V4_4_1, "https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v4.4.1/ffmpeg-4.4.1-win-64.zip"}, - { FFMpegVersions.V4_2_1, "https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v4.2.1/ffmpeg-4.2.1-win-64.zip"}, - { FFMpegVersions.V4_2, "https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v4.2/ffmpeg-4.2-win-64.zip"}, - { FFMpegVersions.V4_1, "https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v4.1/ffmpeg-4.1-win-64.zip"}, - { FFMpegVersions.V4_0, "https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v4.0/ffmpeg-4.0.1-win-64.zip"}, - { FFMpegVersions.V3_4, "https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v3.4/ffmpeg-3.4-win-64.zip"}, - { FFMpegVersions.V3_3, "https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v3.3/ffmpeg-3.3.4-win-64.zip"}, - { FFMpegVersions.V3_2, "https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v3.2/ffmpeg-3.2-win-64.zip"}, - }; - - private static Dictionary Windows32FFMpegDownloadUrls = new() - { - { FFMpegVersions.V4_4_1, "https://example.com/" }, - { FFMpegVersions.V4_2_1, "https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v4.2.1/ffmpeg-4.2.1-win-32.zip"}, - { FFMpegVersions.V4_2, "https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v4.2/ffmpeg-4.2-win-32.zip"}, - { FFMpegVersions.V4_1, "https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v4.1/ffmpeg-4.1-win-32.zip"}, - { FFMpegVersions.V4_0, "https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v4.0/ffmpeg-4.0.1-win-32.zip"}, - { FFMpegVersions.V3_4, "https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v3.4/ffmpeg-3.4-win-32.zip"}, - { FFMpegVersions.V3_3, "https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v3.3/ffmpeg-3.3.4-win-32.zip"}, - { FFMpegVersions.V3_2, "https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v3.2/ffmpeg-3.2-win-32.zip"}, - }; - - public enum FFMpegVersions + /// + /// FFMpeg binary + /// + FFMpeg, + + /// + /// FFProbe binary + /// + FFProbe, + + /// + /// FFPlay binary + /// + FFPlay + } + + public enum FFMpegVersions : ushort { + Latest, V4_4_1, V4_2_1, V4_2, @@ -48,70 +43,71 @@ public enum FFMpegVersions V3_2 } - public static List AutoDownloadFFMpegSuite(FFMpegVersions version = FFMpegVersions.V4_4_1) + /// + /// 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 binary 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) { - var files = AutoDownloadFFMpeg(version); - files.AddRange(AutoDownloadFFProbe(version)); - files.AddRange(AutoDownloadFFPlay(version)); - - return files; + var versionInfo = await GetVersionInfo(version); + var downloadInfo = versionInfo.BinaryInfo?.GetCompatibleDownloadInfo() ?? + 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; } - public static List AutoDownloadFFMpeg(FFMpegVersions version = FFMpegVersions.V4_4_1) - { - var url = Environment.Is64BitProcess - ? new Uri(Windows64FFMpegDownloadUrls[version]) - : new Uri(Windows32FFMpegDownloadUrls[version]); - - HasValidUri(url); - - Stream zipStream = DownloadZip(url); - - return ExtractAndSave(zipStream); - } - - public static List AutoDownloadFFProbe(FFMpegVersions version = FFMpegVersions.V4_4_1) - { - var url = Environment.Is64BitProcess - ? new Uri(Windows64FFMpegDownloadUrls[version].Replace("ffmpeg", "ffprobe")) - : new Uri(Windows32FFMpegDownloadUrls[version].Replace("ffmpeg", "ffprobe")); - - HasValidUri(url); - - Stream zipStream = DownloadZip(url); - - return ExtractAndSave(zipStream); - } - - public static List AutoDownloadFFPlay(FFMpegVersions version = FFMpegVersions.V4_4_1) - { - var url = Environment.Is64BitProcess - ? new Uri(Windows64FFMpegDownloadUrls[version].Replace("ffmpeg", "ffplay")) - : new Uri(Windows32FFMpegDownloadUrls[version].Replace("ffmpeg", "ffplay")); - - HasValidUri(url); - - Stream zipStream = DownloadZip(url); - - return ExtractAndSave(zipStream); - } - - private static MemoryStream DownloadZip(Uri address) + /// + /// Download file from uri + /// + /// uri of the file + /// + private static MemoryStream DownloadFileAsSteam(Uri address) { var client = new WebClient(); - var zipStream = new MemoryStream(client.DownloadData(address)); - zipStream.Position = 0; + var fileStream = new MemoryStream(client.DownloadData(address)); + fileStream.Position = 0; - return zipStream; + return fileStream; } - - private static List ExtractAndSave(Stream zipStream) + + /// + /// Extracts the binaries from the zip stream and saves them to the current binary folder + /// + /// steam of the zip file + /// + private static List 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.exe" or "ffmpeg" or "ffprobe.exe") + 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)); @@ -121,11 +117,128 @@ private static List ExtractAndSave(Stream zipStream) return files; } - private static void HasValidUri(Uri uri) + #region FFbinaries api + + private class DownloadInfo { - if (uri.ToString() == "https://example.com/" || !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + [JsonPropertyName("ffmpeg")] public string? FFMpeg { get; } + + [JsonPropertyName("ffprobe")] public string? FFProbe { get; } + + [JsonPropertyName("ffplay")] public string? FFPlay { get; } + } + + private class BinaryInfo + { + [JsonPropertyName("windows-64")] public DownloadInfo? Windows64 { get; } + + [JsonPropertyName("windows-32")] public DownloadInfo? Windows32 { get; } + + [JsonPropertyName("linux-32")] public DownloadInfo? Linux32 { get; set; } + + [JsonPropertyName("linux-64")] public DownloadInfo? Linux64 { get; } + + [JsonPropertyName("linux-armhf")] public DownloadInfo? LinuxArmhf { get; } + + [JsonPropertyName("linux-armel")] public DownloadInfo? LinuxArmel { get; set; } + + [JsonPropertyName("linux-arm64")] public DownloadInfo? LinuxArm64 { get; } + + [JsonPropertyName("osx-64")] public DownloadInfo? Osx64 { get; } + + public DownloadInfo? GetCompatibleDownloadInfo() { - throw new PlatformNotSupportedException("The requested version of FFMpeg component is not available for your OS."); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return RuntimeInformation.OSArchitecture == Architecture.X64 ? Windows64 : Windows32; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return RuntimeInformation.OSArchitecture switch + { + Architecture.X64 => Linux64, + Architecture.Arm => LinuxArmhf, + Architecture.Arm64 => LinuxArm64, + _ => throw new PlatformNotSupportedException("Unsupported Linux architecture") + }; + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return Osx64; + } + + throw new PlatformNotSupportedException("Unsupported OS"); } } + + 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; } = ""; }