Compare commits

..

No commits in common. "bbdbe5592ad85f49ca96b8b4a24c3ce4bd200dff" and "f919a05d43497fb6b975571aacd6d5bcf31ed25f" have entirely different histories.

14 changed files with 178 additions and 174 deletions

View file

@ -3,7 +3,7 @@
[Flags] [Flags]
public enum FFMpegBinaries : ushort public enum FFMpegBinaries : ushort
{ {
FFMpeg = 1, FFMpeg,
FFProbe = 2, FFProbe,
FFPlay = 4 FFPlay
} }

View file

@ -5,7 +5,7 @@ namespace FFMpegCore.Extensions.Downloader.Enums;
public enum FFMpegVersions : ushort public enum FFMpegVersions : ushort
{ {
[Description("https://ffbinaries.com/api/v1/version/latest")] [Description("https://ffbinaries.com/api/v1/version/latest")]
LatestAvailable, Latest,
[Description("https://ffbinaries.com/api/v1/version/6.1")] [Description("https://ffbinaries.com/api/v1/version/6.1")]
V6_1, V6_1,

View file

@ -5,7 +5,7 @@
/// </summary> /// </summary>
public class FFMpegDownloaderException : Exception public class FFMpegDownloaderException : Exception
{ {
public readonly string Detail = ""; public string Detail { get; set; } = "";
public FFMpegDownloaderException(string message) : base(message) public FFMpegDownloaderException(string message) : base(message)
{ {

View file

@ -2,9 +2,9 @@
namespace FFMpegCore.Extensions.Downloader.Extensions; namespace FFMpegCore.Extensions.Downloader.Extensions;
public static class EnumExtensions internal static class EnumExtensions
{ {
internal static string GetDescription(this Enum enumValue) public static string GetDescription(this Enum enumValue)
{ {
var field = enumValue.GetType().GetField(enumValue.ToString()); var field = enumValue.GetType().GetField(enumValue.ToString());
if (field == null) if (field == null)
@ -19,13 +19,4 @@ public static class EnumExtensions
return enumValue.ToString(); return enumValue.ToString();
} }
public static TEnum[] GetFlags<TEnum>(this TEnum input) where TEnum : Enum
{
return Enum.GetValues(input.GetType())
.Cast<Enum>()
.Where(input.HasFlag)
.Cast<TEnum>()
.ToArray();
}
} }

View file

@ -1,5 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup> <PropertyGroup>
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
<Description>FFMpeg downloader extension for FFMpegCore</Description> <Description>FFMpeg downloader extension for FFMpegCore</Description>
@ -8,7 +13,7 @@
<PackageReleaseNotes> <PackageReleaseNotes>
</PackageReleaseNotes> </PackageReleaseNotes>
<PackageTags>ffmpeg ffprobe convert video audio mediafile resize analyze download</PackageTags> <PackageTags>ffmpeg ffprobe convert video audio mediafile resize analyze download</PackageTags>
<Authors>Kerry Cao, Malte Rosenbjerg</Authors> <Authors>Malte Rosenbjerg, Vlad Jerca, Max Bagryantsev, Kerry Cao</Authors>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View file

@ -1,83 +1,53 @@
using System.IO.Compression; using FFMpegCore.Extensions.Downloader.Enums;
using System.Text.Json;
using FFMpegCore.Extensions.Downloader.Enums;
using FFMpegCore.Extensions.Downloader.Exceptions; using FFMpegCore.Extensions.Downloader.Exceptions;
using FFMpegCore.Extensions.Downloader.Extensions; using FFMpegCore.Extensions.Downloader.Services;
using FFMpegCore.Extensions.Downloader.Models;
namespace FFMpegCore.Extensions.Downloader; namespace FFMpegCore.Extensions.Downloader;
public static class FFMpegDownloader public class FFMpegDownloader
{ {
/// <summary> /// <summary>
/// Download the latest FFMpeg suite binaries for current platform /// Download the latest FFMpeg suite binaries for current platform
/// </summary> /// </summary>
/// <param name="version">used to explicitly state the version of binary you want to download</param> /// <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="binaries">used to explicitly state the binaries you want to download (ffmpeg, ffprobe, ffplay)</param>
/// <param name="options">used for specifying binary folder to download binaries into. If not provided, GlobalFFOptions are used</param>
/// <param name="platformOverride">used to explicitly state the os and architecture you want to download</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> /// <returns>a list of the binaries that have been successfully downloaded</returns>
public static async Task<List<string>> DownloadBinaries( public static async Task<List<string>> DownloadFFMpegSuite(
FFMpegVersions version = FFMpegVersions.LatestAvailable, FFMpegVersions version = FFMpegVersions.Latest,
FFMpegBinaries binaries = FFMpegBinaries.FFMpeg | FFMpegBinaries.FFProbe, FFMpegBinaries binaries = FFMpegBinaries.FFMpeg | FFMpegBinaries.FFProbe,
FFOptions? options = null,
SupportedPlatforms? platformOverride = null) SupportedPlatforms? platformOverride = null)
{ {
using var httpClient = new HttpClient(); // get all available versions
var versionInfo = await FFbinariesService.GetVersionInfo(version);
var versionInfo = await httpClient.GetVersionInfo(version); // get the download info for the current platform
var binariesDictionary = versionInfo.BinaryInfo?.GetCompatibleDownloadInfo(platformOverride) ?? var downloadInfo = versionInfo.BinaryInfo?.GetCompatibleDownloadInfo(platformOverride) ??
throw new FFMpegDownloaderException("Failed to get compatible download info"); throw new FFMpegDownloaderException("Failed to get compatible download info");
var successList = new List<string>(); var successList = new List<string>();
var relevantOptions = options ?? GlobalFFOptions.Current;
if (string.IsNullOrEmpty(relevantOptions.BinaryFolder)) // download ffmpeg if selected
if (binaries.HasFlag(FFMpegBinaries.FFMpeg) && downloadInfo.FFMpeg is not null)
{ {
throw new FFMpegDownloaderException("Binary folder not specified"); await using var zipStream = await FFbinariesService.DownloadFileAsSteam(new Uri(downloadInfo.FFMpeg));
successList.AddRange(FFbinariesService.ExtractZipAndSave(zipStream));
} }
var binaryFlags = binaries.GetFlags(); // download ffprobe if selected
foreach (var binaryFlag in binaryFlags) if (binaries.HasFlag(FFMpegBinaries.FFProbe) && downloadInfo.FFProbe is not null)
{ {
if (binariesDictionary.TryGetValue(binaryFlag.ToString().ToLowerInvariant(), out var binaryUrl)) await using var zipStream = await FFbinariesService.DownloadFileAsSteam(new Uri(downloadInfo.FFProbe));
{ successList.AddRange(FFbinariesService.ExtractZipAndSave(zipStream));
using var zipStream = await httpClient.GetStreamAsync(new Uri(binaryUrl)); }
var extracted = ExtractZipAndSave(zipStream, relevantOptions.BinaryFolder);
successList.AddRange(extracted); // 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; return successList;
} }
private static async Task<VersionInfo> GetVersionInfo(this HttpClient client, FFMpegVersions version)
{
var versionUri = version.GetDescription();
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);
}
private static IEnumerable<string> ExtractZipAndSave(Stream zipStream, string binaryFolder)
{
using var archive = new ZipArchive(zipStream, ZipArchiveMode.Read);
foreach (var entry in archive.Entries)
{
if (entry.Name is "ffmpeg" or "ffmpeg.exe" or "ffprobe.exe" or "ffprobe" or "ffplay.exe" or "ffplay")
{
var filePath = Path.Combine(binaryFolder, entry.Name);
entry.ExtractToFile(filePath, true);
yield return filePath;
}
}
}
} }

View file

@ -4,23 +4,23 @@ using FFMpegCore.Extensions.Downloader.Enums;
namespace FFMpegCore.Extensions.Downloader.Models; namespace FFMpegCore.Extensions.Downloader.Models;
internal class BinaryInfo internal record BinaryInfo
{ {
[JsonPropertyName("windows-64")] public Dictionary<string, string>? Windows64 { get; set; } [JsonPropertyName("windows-64")] public DownloadInfo? Windows64 { get; set; }
[JsonPropertyName("windows-32")] public Dictionary<string, string>? Windows32 { get; set; } [JsonPropertyName("windows-32")] public DownloadInfo? Windows32 { get; set; }
[JsonPropertyName("linux-32")] public Dictionary<string, string>? Linux32 { get; set; } [JsonPropertyName("linux-32")] public DownloadInfo? Linux32 { get; set; }
[JsonPropertyName("linux-64")] public Dictionary<string, string>? Linux64 { get; set; } [JsonPropertyName("linux-64")] public DownloadInfo? Linux64 { get; set; }
[JsonPropertyName("linux-armhf")] public Dictionary<string, string>? LinuxArmhf { get; set; } [JsonPropertyName("linux-armhf")] public DownloadInfo? LinuxArmhf { get; set; }
[JsonPropertyName("linux-armel")] public Dictionary<string, string>? LinuxArmel { get; set; } [JsonPropertyName("linux-armel")] public DownloadInfo? LinuxArmel { get; set; }
[JsonPropertyName("linux-arm64")] public Dictionary<string, string>? LinuxArm64 { get; set; } [JsonPropertyName("linux-arm64")] public DownloadInfo? LinuxArm64 { get; set; }
[JsonPropertyName("osx-64")] public Dictionary<string, string>? Osx64 { get; set; } [JsonPropertyName("osx-64")] public DownloadInfo? Osx64 { get; set; }
/// <summary> /// <summary>
/// Automatically get the compatible download info for current os and architecture /// Automatically get the compatible download info for current os and architecture
@ -29,7 +29,7 @@ internal class BinaryInfo
/// <returns></returns> /// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception> /// <exception cref="ArgumentOutOfRangeException"></exception>
/// <exception cref="PlatformNotSupportedException"></exception> /// <exception cref="PlatformNotSupportedException"></exception>
public Dictionary<string, string>? GetCompatibleDownloadInfo(SupportedPlatforms? platformOverride = null) public DownloadInfo? GetCompatibleDownloadInfo(SupportedPlatforms? platformOverride = null)
{ {
if (platformOverride is not null) if (platformOverride is not null)
{ {
@ -64,7 +64,7 @@ internal class BinaryInfo
}; };
} }
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && RuntimeInformation.OSArchitecture == Architecture.X64) if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{ {
return Osx64; return Osx64;
} }

View file

@ -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; }
}

View file

@ -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;
/// <summary>
/// Service to interact with ffbinaries.com API
/// </summary>
internal class FFbinariesService
{
/// <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>
internal static async Task<VersionInfo> 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<VersionInfo>(jsonString);
return versionInfo ??
throw new FFMpegDownloaderException($"Failed to deserialize version info from {versionUri}", jsonString);
}
/// <summary>
/// Download file from uri
/// </summary>
/// <param name="address">uri of the file</param>
/// <returns></returns>
internal static async Task<Stream> DownloadFileAsSteam(Uri address)
{
var client = new HttpClient();
return await client.GetStreamAsync(address);
}
/// <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>
internal 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;
}
}

View file

@ -1,53 +1,22 @@
using FFMpegCore.Extensions.Downloader; using FFMpegCore.Extensions.Downloader;
using FFMpegCore.Extensions.Downloader.Enums; using FFMpegCore.Extensions.Downloader.Enums;
using FFMpegCore.Test.Utilities;
namespace FFMpegCore.Test; namespace FFMpegCore.Test;
[TestClass] [TestClass]
public class DownloaderTests public class DownloaderTests
{ {
private FFOptions _ffOptions; [TestMethod]
[TestInitialize]
public void InitializeTestFolder()
{
var tempDownloadFolder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(tempDownloadFolder);
_ffOptions = new FFOptions { BinaryFolder = tempDownloadFolder };
}
[TestCleanup]
public void DeleteTestFolder()
{
Directory.Delete(_ffOptions.BinaryFolder, true);
}
[OsSpecificTestMethod(OsPlatforms.Windows | OsPlatforms.Linux)]
public async Task GetSpecificVersionTest() public async Task GetSpecificVersionTest()
{ {
var binaries = await FFMpegDownloader.DownloadBinaries(FFMpegVersions.V6_1, options: _ffOptions); var binaries = await FFMpegDownloader.DownloadFFMpegSuite(FFMpegVersions.V6_1);
try Assert.HasCount(2, binaries);
{
Assert.HasCount(2, binaries);
}
finally
{
binaries.ForEach(File.Delete);
}
} }
[OsSpecificTestMethod(OsPlatforms.Windows | OsPlatforms.Linux)] [TestMethod]
public async Task GetAllLatestSuiteTest() public async Task GetAllLatestSuiteTest()
{ {
var binaries = await FFMpegDownloader.DownloadBinaries(options: _ffOptions); var binaries = await FFMpegDownloader.DownloadFFMpegSuite();
try Assert.HasCount(2, binaries);
{
Assert.HasCount(2, binaries);
}
finally
{
binaries.ForEach(File.Delete);
}
} }
} }

View file

@ -1,42 +0,0 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using FFMpegCore.Extensions.Downloader.Extensions;
namespace FFMpegCore.Test.Utilities;
[Flags]
internal enum OsPlatforms : ushort
{
Windows = 1,
Linux = 2,
MacOS = 4
}
internal class OsSpecificTestMethod : TestMethodAttribute
{
private readonly IEnumerable<OSPlatform> _supportedOsPlatforms;
public OsSpecificTestMethod(OsPlatforms supportedOsPlatforms, [CallerFilePath] string callerFilePath = "",
[CallerLineNumber] int callerLineNumber = -1) : base(callerFilePath, callerLineNumber)
{
_supportedOsPlatforms = supportedOsPlatforms.GetFlags()
.Select(flag => OSPlatform.Create(flag.ToString().ToUpperInvariant()))
.ToArray();
}
public override async Task<TestResult[]> ExecuteAsync(ITestMethod testMethod)
{
if (_supportedOsPlatforms.Any(RuntimeInformation.IsOSPlatform))
{
return await base.ExecuteAsync(testMethod);
}
var message = $"Test only executed on specific platforms: {string.Join(", ", _supportedOsPlatforms.Select(platform => platform.ToString()))}";
{
return
[
new TestResult { Outcome = UnitTestOutcome.Inconclusive, TestFailureException = new AssertInconclusiveException(message) }
];
}
}
}

View file

@ -0,0 +1,28 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace FFMpegCore.Test.Utilities;
public class WindowsOnlyTestMethod : TestMethodAttribute
{
public WindowsOnlyTestMethod([CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = -1)
: base(callerFilePath, callerLineNumber)
{
}
public override async Task<TestResult[]> ExecuteAsync(ITestMethod testMethod)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var message = "Test not executed on other platforms than Windows";
{
return
[
new TestResult { Outcome = UnitTestOutcome.Inconclusive, TestFailureException = new AssertInconclusiveException(message) }
];
}
}
return await base.ExecuteAsync(testMethod);
}
}

View file

@ -93,7 +93,7 @@ public class VideoTest
} }
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
[OsSpecificTestMethod(OsPlatforms.Windows)] [WindowsOnlyTestMethod]
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
[DataRow(PixelFormat.Format24bppRgb)] [DataRow(PixelFormat.Format24bppRgb)]
[DataRow(PixelFormat.Format32bppArgb)] [DataRow(PixelFormat.Format32bppArgb)]
@ -125,7 +125,7 @@ public class VideoTest
} }
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
[OsSpecificTestMethod(OsPlatforms.Windows)] [WindowsOnlyTestMethod]
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
public void Video_ToMP4_Args_Pipe_DifferentImageSizes_WindowsOnly() public void Video_ToMP4_Args_Pipe_DifferentImageSizes_WindowsOnly()
{ {
@ -157,7 +157,7 @@ public class VideoTest
} }
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
[OsSpecificTestMethod(OsPlatforms.Windows)] [WindowsOnlyTestMethod]
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
public async Task Video_ToMP4_Args_Pipe_DifferentImageSizes_WindowsOnly_Async() public async Task Video_ToMP4_Args_Pipe_DifferentImageSizes_WindowsOnly_Async()
{ {
@ -189,7 +189,7 @@ public class VideoTest
} }
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
[OsSpecificTestMethod(OsPlatforms.Windows)] [WindowsOnlyTestMethod]
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
public void Video_ToMP4_Args_Pipe_DifferentPixelFormats_WindowsOnly() public void Video_ToMP4_Args_Pipe_DifferentPixelFormats_WindowsOnly()
{ {
@ -222,7 +222,7 @@ public class VideoTest
} }
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
[OsSpecificTestMethod(OsPlatforms.Windows)] [WindowsOnlyTestMethod]
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
public async Task Video_ToMP4_Args_Pipe_DifferentPixelFormats_WindowsOnly_Async() public async Task Video_ToMP4_Args_Pipe_DifferentPixelFormats_WindowsOnly_Async()
{ {
@ -397,7 +397,7 @@ public class VideoTest
} }
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
[OsSpecificTestMethod(OsPlatforms.Windows)] [WindowsOnlyTestMethod]
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
[DataRow(PixelFormat.Format24bppRgb)] [DataRow(PixelFormat.Format24bppRgb)]
[DataRow(PixelFormat.Format32bppArgb)] [DataRow(PixelFormat.Format32bppArgb)]
@ -446,7 +446,7 @@ public class VideoTest
} }
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
[OsSpecificTestMethod(OsPlatforms.Windows)] [WindowsOnlyTestMethod]
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
[DataRow(SKColorType.Rgb565)] [DataRow(SKColorType.Rgb565)]
[DataRow(SKColorType.Bgra8888)] [DataRow(SKColorType.Bgra8888)]
@ -483,7 +483,7 @@ public class VideoTest
} }
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
[OsSpecificTestMethod(OsPlatforms.Windows)] [WindowsOnlyTestMethod]
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
[DataRow(PixelFormat.Format24bppRgb)] [DataRow(PixelFormat.Format24bppRgb)]
[DataRow(PixelFormat.Format32bppArgb)] [DataRow(PixelFormat.Format32bppArgb)]
@ -516,7 +516,7 @@ public class VideoTest
} }
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
[OsSpecificTestMethod(OsPlatforms.Windows)] [WindowsOnlyTestMethod]
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
public void Video_Snapshot_InMemory_SystemDrawingCommon() public void Video_Snapshot_InMemory_SystemDrawingCommon()
{ {
@ -840,7 +840,7 @@ public class VideoTest
} }
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
[OsSpecificTestMethod(OsPlatforms.Windows)] [WindowsOnlyTestMethod]
[Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)] [Timeout(BaseTimeoutMilliseconds, CooperativeCancellation = true)]
public void Video_TranscodeInMemory_WindowsOnly() public void Video_TranscodeInMemory_WindowsOnly()
{ {

View file

@ -187,7 +187,7 @@ If you want to use `System.Drawing.Bitmap`s as `IVideoFrame`s, a `BitmapVideoFra
## Runtime Auto Installation ## Runtime Auto Installation
You can install a version of ffmpeg suite at runtime using `FFMpegDownloader.DownloadFFMpegSuite();` You can install a version of ffmpeg suite at runtime using `FFMpegDownloader.DownloadFFMpegSuite();`
This feature uses the api from [ffbinaries](https://ffbinaries.com/api). This feature uses the api from [ffbinaries](https://ffbinaries.com/api).
## Manual Installation ## Manual Installation