Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to install ffmpeg suite at runtime added with FFMpegDownloader #442

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9db4ba7
features to auto download ffmpeg binaries added
yuqian5 Apr 12, 2023
7c333a7
Merge pull request #1 from rosenbjerg/main
yuqian5 Apr 12, 2023
a4bb69a
changes made to support multiple version downloads for windows
yuqian5 Apr 12, 2023
d44c77c
Merge remote-tracking branch 'origin/main'
yuqian5 Apr 12, 2023
657ee5f
Comments added
May 5, 2023
d68ce8e
Methods renamed to remove auto prefix
May 5, 2023
debb868
README.md updated to include ffmpegdownloader usage
May 5, 2023
a647f44
refactoring and reformatting
May 5, 2023
463fb9b
Reformatted to fix lint error
May 5, 2023
cf7fd2f
Merge branch 'main' into main
yuqian5 May 7, 2023
97fcd3b
Merge branch 'main' into main
yuqian5 Jun 4, 2023
da7d1fa
support added for linux, macos
yuqian5 Sep 6, 2023
ae92e93
Merge remote-tracking branch 'origin/main'
yuqian5 Sep 6, 2023
df8d97e
allow os and architecture overrider
yuqian5 Sep 6, 2023
937db76
format fix for lint
yuqian5 Sep 6, 2023
dfc486d
ffmpeg downloader moved to seperate package
Sep 6, 2023
d978d7d
ffmpeg downloader moved to separate package
Sep 6, 2023
2b66b82
Merge remote-tracking branch 'origin/main'
yuqian5 Sep 6, 2023
ae5ae97
FFMpegCore.Downloader renamed to FFMpegCore.Extensions.Downloader
yuqian5 Sep 6, 2023
882cdf8
Merge branch 'main' into main
rosenbjerg Oct 5, 2023
bba4a9f
Merge branch 'main' into main
rosenbjerg Oct 5, 2023
5bedbdb
Update README.md
yuqian5 Oct 10, 2023
22fa002
Debug for CI failure
yuqian5 Oct 10, 2023
6524b1c
Update DownloaderTests.cs
yuqian5 Oct 10, 2023
105a26b
Merge branch 'rosenbjerg:main' into main
yuqian5 Jan 4, 2024
09ae944
version 5.1 and 6.1 added
yuqian5 Jan 4, 2024
ac3794e
Merge branch 'main' into main
rosenbjerg Dec 4, 2024
a91e309
Merge branch 'main' into main
rosenbjerg Dec 4, 2024
c44170b
Merge branch 'main' into main
rosenbjerg Dec 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>

<PropertyGroup>
<IsPackable>true</IsPackable>
<Description>FFMpeg downloader extension for FFMpegCore</Description>
<PackageVersion>5.0.0</PackageVersion>
<PackageOutputPath>../nupkg</PackageOutputPath>
<PackageReleaseNotes>
</PackageReleaseNotes>
<PackageTags>ffmpeg ffprobe convert video audio mediafile resize analyze download</PackageTags>
<Authors>Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev, Kerry Cao</Authors>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\FFMpegCore\FFMpegCore.csproj"/>
</ItemGroup>

</Project>
276 changes: 276 additions & 0 deletions FFMpegCore.Extensions.Downloader/FFMpegDownloader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
using System.IO.Compression;
using System.Net;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace FFMpegCore.Extensions.Downloader;

/// <summary>
/// Downloads the latest FFMpeg suite binaries from ffbinaries.com.
/// </summary>
public class FFMpegDownloader
{
[Flags]
public enum FFMpegBinaries : ushort
{
FFMpeg,
FFProbe,
FFPlay
}

public enum FFMpegVersions : ushort
{
Latest,
V6_1,
V5_1,
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
}

/// <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 binaries you want to download (ffmpeg, ffprobe, ffplay)</param>
/// <param name="platformOverride">used to explicitly state the os and architecture 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,
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<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;
}

/// <summary>
/// Download file from uri
/// </summary>
/// <param name="address">uri of the file</param>
/// <returns></returns>
private static MemoryStream DownloadFileAsSteam(Uri address)
{
var client = new WebClient();
var fileStream = new MemoryStream(client.DownloadData(address));
fileStream.Position = 0;

return fileStream;
}

/// <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 IEnumerable<string> ExtractZipAndSave(Stream zipStream)
{
using var archive = new ZipArchive(zipStream, ZipArchiveMode.Read);
List<string> 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; }

/// <summary>
/// Automatically get the compatible download info for current os and architecture
/// </summary>
/// <param name="platformOverride"></param>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
/// <exception cref="PlatformNotSupportedException"></exception>
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<FFMpegVersions, string> _FFBinariesAPIs = new()
{
{ FFMpegVersions.Latest, "https://ffbinaries.com/api/v1/version/latest" },
{ FFMpegVersions.V6_1, "https://ffbinaries.com/api/v1/version/6.1" },
{ FFMpegVersions.V5_1, "https://ffbinaries.com/api/v1/version/5.1" },
{ 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; } = "";
}
22 changes: 22 additions & 0 deletions FFMpegCore.Test/DownloaderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using FFMpegCore.Extensions.Downloader;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace FFMpegCore.Test;

[TestClass]
public class DownloaderTests
{
[TestMethod]
public void GetAllLatestSuiteTest()
{
var binaries = FFMpegDownloader.DownloadFFMpegSuite(binaries: FFMpegDownloader.FFMpegBinaries.FFMpeg).Result;
Assert.IsTrue(binaries.Count == 1); // many platforms have only ffmpeg and ffprobe
}

[TestMethod]
public void GetSpecificVersionTest()
{
var binaries = FFMpegDownloader.DownloadFFMpegSuite(FFMpegDownloader.FFMpegVersions.V4_0, binaries: FFMpegDownloader.FFMpegBinaries.FFMpeg).Result;
Assert.IsTrue(binaries.Count == 1);
}
}
1 change: 1 addition & 0 deletions FFMpegCore.Test/FFMpegCore.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\FFMpegCore.Extensions.Downloader\FFMpegCore.Extensions.Downloader.csproj" />
<ProjectReference Include="..\FFMpegCore.Extensions.SkiaSharp\FFMpegCore.Extensions.SkiaSharp.csproj" />
<ProjectReference Include="..\FFMpegCore.Extensions.System.Drawing.Common\FFMpegCore.Extensions.System.Drawing.Common.csproj" />
<ProjectReference Include="..\FFMpegCore\FFMpegCore.csproj" />
Expand Down
10 changes: 8 additions & 2 deletions FFMpegCore.sln
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading