support added for linux, macos

win32, win64, lnx32, lnx64, lnx-armhf, lnx-armel, lnx-arm64, osx64
This commit is contained in:
Kerry Cao 2023-09-06 01:25:51 -06:00
parent d44c77c958
commit da7d1fafed
2 changed files with 209 additions and 137 deletions

View file

@ -1,5 +1,4 @@
using System.Runtime.InteropServices; using FFMpegCore.Helpers;
using FFMpegCore.Helpers;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace FFMpegCore.Test; namespace FFMpegCore.Test;
@ -8,58 +7,18 @@ namespace FFMpegCore.Test;
public class DownloaderTests public class DownloaderTests
{ {
[TestMethod] [TestMethod]
public void GetLatestSuiteTest() public void GetAllLatestSuiteTest()
{ {
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) var binaries = FFMpegDownloader.DownloadFFMpegSuite(binaries: FFMpegDownloader.FFMpegBinaries.FFProbe |
{ FFMpegDownloader.FFMpegBinaries.FFMpeg |
var fileNames = FFMpegDownloader.AutoDownloadFFMpegSuite(); FFMpegDownloader.FFMpegBinaries.FFPlay).Result;
Assert.IsTrue(fileNames.Count == 3); Assert.IsTrue(binaries.Count >= 2); // many platforms have only ffmpeg and ffprobe
}
else
{
Assert.Inconclusive("This test is only for Windows");
}
} }
[TestMethod] [TestMethod]
public void GetLatestFFMpegTest() public void GetSpecificVersionTest()
{ {
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) var binaries = FFMpegDownloader.DownloadFFMpegSuite(FFMpegDownloader.FFMpegVersions.V4_0).Result;
{ Assert.IsTrue(binaries.Count == 2);
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");
}
} }
} }

View file

@ -1,43 +1,38 @@
using System.ComponentModel; using System.IO.Compression;
using System.Net; using System.Net;
using System.IO; using System.Runtime.InteropServices;
using System.IO.Compression; using System.Text.Json;
using System.Text.Json.Serialization;
namespace FFMpegCore.Helpers; namespace FFMpegCore.Helpers;
using System.Runtime.InteropServices;
/// <summary> /// <summary>
/// Downloads the latest FFMpeg suite binaries from GitHub. Only supported for windows at the moment. /// Downloads the latest FFMpeg suite binaries from ffbinaries.com.
/// </summary> /// </summary>
public class FFMpegDownloader // this class is built to be easily modified to support other platforms public class FFMpegDownloader
{ {
private static Dictionary<FFMpegVersions, string> 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"}, /// <summary>
{ FFMpegVersions.V4_2_1, "https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v4.2.1/ffmpeg-4.2.1-win-64.zip"}, /// FFMpeg binary
{ FFMpegVersions.V4_2, "https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v4.2/ffmpeg-4.2-win-64.zip"}, /// </summary>
{ FFMpegVersions.V4_1, "https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v4.1/ffmpeg-4.1-win-64.zip"}, FFMpeg,
{ 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"}, /// <summary>
{ FFMpegVersions.V3_3, "https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v3.3/ffmpeg-3.3.4-win-64.zip"}, /// FFProbe binary
{ FFMpegVersions.V3_2, "https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v3.2/ffmpeg-3.2-win-64.zip"}, /// </summary>
}; FFProbe,
private static Dictionary<FFMpegVersions, string> Windows32FFMpegDownloadUrls = new() /// <summary>
{ /// FFPlay binary
{ FFMpegVersions.V4_4_1, "https://example.com/" }, /// </summary>
{ FFMpegVersions.V4_2_1, "https://github.com/ffbinaries/ffbinaries-prebuilt/releases/download/v4.2.1/ffmpeg-4.2.1-win-32.zip"}, FFPlay
{ 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"}, public enum FFMpegVersions : ushort
{ 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
{ {
Latest,
V4_4_1, V4_4_1,
V4_2_1, V4_2_1,
V4_2, V4_2,
@ -48,70 +43,71 @@ public enum FFMpegVersions
V3_2 V3_2
} }
public static List<string> AutoDownloadFFMpegSuite(FFMpegVersions version = FFMpegVersions.V4_4_1) /// <summary>
/// Download the latest FFMpeg suite binaries for current platform
/// </summary>
/// <param name="version">used to explicitly state the version of binary you want to download</param>
/// <param name="binaries">used to explicitly state the binary you want to download</param>
/// <returns>a list of the binaries that have been successfully downloaded</returns>
public static async Task<List<string>> DownloadFFMpegSuite(FFMpegVersions version = FFMpegVersions.Latest,
FFMpegBinaries binaries = FFMpegBinaries.FFMpeg | FFMpegBinaries.FFProbe)
{ {
var files = AutoDownloadFFMpeg(version); var versionInfo = await GetVersionInfo(version);
files.AddRange(AutoDownloadFFProbe(version)); var downloadInfo = versionInfo.BinaryInfo?.GetCompatibleDownloadInfo() ??
files.AddRange(AutoDownloadFFPlay(version)); throw new FFMpegDownloaderException("Failed to get compatible download info");
return files; var successList = new List<string>();
// 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<string> AutoDownloadFFMpeg(FFMpegVersions version = FFMpegVersions.V4_4_1) /// <summary>
{ /// Download file from uri
var url = Environment.Is64BitProcess /// </summary>
? new Uri(Windows64FFMpegDownloadUrls[version]) /// <param name="address">uri of the file</param>
: new Uri(Windows32FFMpegDownloadUrls[version]); /// <returns></returns>
private static MemoryStream DownloadFileAsSteam(Uri address)
HasValidUri(url);
Stream zipStream = DownloadZip(url);
return ExtractAndSave(zipStream);
}
public static List<string> 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<string> 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)
{ {
var client = new WebClient(); var client = new WebClient();
var zipStream = new MemoryStream(client.DownloadData(address)); var fileStream = new MemoryStream(client.DownloadData(address));
zipStream.Position = 0; fileStream.Position = 0;
return zipStream; return fileStream;
} }
private static List<string> ExtractAndSave(Stream zipStream) /// <summary>
/// Extracts the binaries from the zip stream and saves them to the current binary folder
/// </summary>
/// <param name="zipStream">steam of the zip file</param>
/// <returns></returns>
private static List<string> ExtractZipAndSave(Stream zipStream)
{ {
using var archive = new ZipArchive(zipStream, ZipArchiveMode.Read); using var archive = new ZipArchive(zipStream, ZipArchiveMode.Read);
List<string> files = new(); List<string> files = new();
foreach (var entry in archive.Entries) 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); entry.ExtractToFile(Path.Combine(GlobalFFOptions.Current.BinaryFolder, entry.Name), true);
files.Add(Path.Combine(GlobalFFOptions.Current.BinaryFolder, entry.Name)); files.Add(Path.Combine(GlobalFFOptions.Current.BinaryFolder, entry.Name));
@ -121,11 +117,128 @@ private static List<string> ExtractAndSave(Stream zipStream)
return files; 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<FFMpegVersions, string> 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" }
};
/// <summary>
/// Get version info from ffbinaries.com
/// </summary>
/// <param name="version">use to explicitly state the version of ffmpeg you want</param>
/// <returns></returns>
/// <exception cref="FFMpegDownloaderException"></exception>
private static async Task<VersionInfo> 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<VersionInfo>(jsonString);
return versionInfo ??
throw new FFMpegDownloaderException($"Failed to deserialize version info from {versionUri}", jsonString);
}
#endregion
}
/// <summary>
/// Custom exception for FFMpegDownloader
/// </summary>
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; } = "";
} }