diff --git a/README.md b/README.md index ce8bbc0..c69eef6 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,28 @@ # XamarinAndroidFFmpeg (Xamarin.FFmpeg) -FFmpeg library to run ffmeg commands over Xamarin.Android. +FFmpeg library to run ffmeg commands over Xamarin.Forms ## About - +* Created to support a solution that needed ffmpeg to control camera devices. +* Edited to allow usage on Xamarin.Forms ## Big thanks - +* FFmpeg library adapted from https://github.com/neurospeech/xamarin-android-ffmpeg +* Original library in java https://github.com/WritingMinds/ffmpeg-android-java ## License - +* MIT +* Besides that, to use this library you must accept the licensing terms mentioned in the source project at https://github.com/WritingMinds/ffmpeg-android-java ## Nuget package You can download Xamarin.FFmpeg package from Nuget Package Manager or run following command in Nuget Package Console. ``` -Install-Package Xamarin.FFmpeg -Version 1.0.4 +Install-Package Xamarin.FFmpeg ``` ## Usage -On the way... + +### Android + +In MainActivity.cs before ``Forms.Init(this, bundle)``: +```cs +Xamarin.FFmpeg.Android.Init.Initialize(this.BaseContext); +``` diff --git a/Xamarin.FFmpeg.Android/FFMpegSource.cs b/Xamarin.FFmpeg.Android/FFMpegSource.cs new file mode 100644 index 0000000..d61f6a3 --- /dev/null +++ b/Xamarin.FFmpeg.Android/FFMpegSource.cs @@ -0,0 +1,51 @@ +using System; + +namespace Xamarin.FFmpeg.Android +{ + public class FFmpegSource : IFFmpegSource + { + public string FFmpegVersion { get; } = "3.0.1.1"; + public string Url { get; set; } + public string Arch { get; } + public string Hash { get; } + public Func IsArch { get; } + + public FFmpegSource(string arch, Func isArch, string hash) + { + Arch = arch; + IsArch = isArch; + Hash = hash; + Url = $"https://raw.githubusercontent.com/gperozzo/XamarinAndroidFFmpeg/master/binary/{FFmpegVersion}/{Arch}/ffmpeg"; + } + + public static FFmpegSource[] Sources = new FFmpegSource[] { + new FFmpegSource("arm", x=> !x.EndsWith("86"), "yRVoeaZATQdZIR/lZxMsIa/io9U="), + new FFmpegSource("x86", x=> x.EndsWith("86"), "mU4QKhrLEO0aROb9N7JOCJ/rVTA==") + }; + + internal static FFmpegSource Get() + { + string osArchitecture = Java.Lang.JavaSystem.GetProperty("os.arch"); + + foreach (var source in Sources) + { + if (source.IsArch(osArchitecture)) + return source; + } + + return null; + } + + public void SetUrl(string url) + { + Url = url; + } + + public bool IsHashMatch(byte[] data) + { + var sha = System.Security.Cryptography.SHA1.Create(); + string h = Convert.ToBase64String(sha.ComputeHash(data)); + return h == Hash; + } + } +} \ No newline at end of file diff --git a/Xamarin.FFmpeg.Android/FFmpegLibrary.cs b/Xamarin.FFmpeg.Android/FFmpegLibrary.cs new file mode 100644 index 0000000..67585f3 --- /dev/null +++ b/Xamarin.FFmpeg.Android/FFmpegLibrary.cs @@ -0,0 +1,315 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Android.App; +using Android.Content; +using System.Threading.Tasks; +using Xamarin.FFmpeg.Exceptions; +using Xamarin.FFmpeg; +using Java.IO; + +namespace Xamarin.FFmpeg.Android +{ + public class FFmpegLibrary : IFFmpegLibrary + { + public string EndOfFFMPEGLine { get; } = "final ratefactor:"; + private string Url; + private string SourceFolder; + private string DownloadTitle; + private bool Initialized = false; + private File FFmpegFile; + internal Context Context; + + /// + /// Initializes the FFmpeg library and download it if necessary + /// + /// Context + /// + /// + /// + public async Task Init() + { + if (Initialized) + { + return; + } + FFmpegFile = new File((SourceFolder ?? Context.FilesDir.AbsolutePath) + "/ffmpeg"); + + FFmpegSource source = FFmpegSource.Get(); + + if (source == null) + { + throw new FFmpegNotInitializedException(); + } + + if (Url != null) + { + source.SetUrl(Url); + } + + await Task.Run(() => + { + if (FFmpegFile.Exists()) + { + try + { + if (source.IsHashMatch(System.IO.File.ReadAllBytes(FFmpegFile.CanonicalPath))) + { + if (!FFmpegFile.CanExecute()) + { + FFmpegFile.SetExecutable(true); + } + + Initialized = true; + + return; + } + } + catch (Exception) + { + // Não implementado + } + + if (FFmpegFile.CanExecute()) + { + FFmpegFile.SetExecutable(false); + } + + FFmpegFile.Delete(); + } + }); + + if (Initialized) + { + // Ffmpeg file exists... + return; + } + + if (FFmpegFile.Exists()) + { + FFmpegFile.Delete(); + } + + await Download(); + + if (!FFmpegFile.CanExecute()) + { + FFmpegFile.SetExecutable(true); + } + + Initialized = true; + } + + /// + /// Run a command in FFmpeg (must be executed in the UI thread) + /// + /// + /// + /// + /// + public async Task Run(string cmd, Action logger = null) + { + try + { + TaskCompletionSource source = new TaskCompletionSource(); + + await Init(); + + await Task.Run(() => + { + try + { + int n = _Run(cmd, logger); + source.SetResult(n); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine(ex); + source.SetException(ex); + } + }); + + return await source.Task; + } + catch (Exception) + { + throw; + } + } + + private int _Run(string cmd, Action logger = null) + { + TaskCompletionSource task = new TaskCompletionSource(); + + var startInfo = new System.Diagnostics.ProcessStartInfo(FFmpegFile.CanonicalPath, cmd) + { + RedirectStandardError = true, + RedirectStandardOutput = true, + UseShellExecute = false + }; + + var process = new System.Diagnostics.Process + { + StartInfo = startInfo + }; + + bool finished = false; + + string error = null; + + process.Start(); + + Task.Run(() => + { + try + { + using (var reader = process.StandardError) + { + StringBuilder processOutput = new StringBuilder(); + while (!finished) + { + var line = reader.ReadLine(); + if (line == null) + break; + logger?.Invoke(line); + processOutput.Append(line); + + if (line.StartsWith(EndOfFFMPEGLine)) + { + Task.Run(async () => + { + await Task.Delay(TimeSpan.FromMinutes(1)); + finished = true; + }); + } + } + error = processOutput.ToString(); + } + } + catch (Exception) + { + // Não implementado + } + }); + + while (!finished) + { + process.WaitForExit(10000); + if (process.HasExited) + { + break; + } + } + + return process.ExitCode; + } + + /// + /// Download the FFmpeg library + /// + /// + /// + /// + public async Task Download() + { + File ffmpegFile; + + if (SourceFolder != null) + { + ffmpegFile = new File(SourceFolder + "/ffmpeg"); + } + else + { + var filesDir = Context.FilesDir; + + ffmpegFile = new File(filesDir + "/ffmpeg"); + } + + FFmpegSource source = FFmpegSource.Get(); + + if (source == null) + { + throw new FFmpegNotInitializedException(); + } + + if (Url != null) + { + source.SetUrl(Url); + } + + try + { + using (var c = new System.Net.Http.HttpClient()) + { + using (var fout = System.IO.File.OpenWrite(ffmpegFile.AbsolutePath)) + { + string url = source.Url; + + var g = new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Get, url); + + var h = await c.SendAsync(g, System.Net.Http.HttpCompletionOption.ResponseHeadersRead); + + var buffer = new byte[51200]; + + var s = await h.Content.ReadAsStreamAsync(); + long total = h.Content.Headers.ContentLength.GetValueOrDefault(); + + IEnumerable sl; + + if (h.Headers.TryGetValues("Content-Length", out sl)) + { + if (total == 0 && sl.Any()) + { + long.TryParse(sl.FirstOrDefault(), out total); + } + } + + int count = 0; + int progress = 0; + + while ((count = await s.ReadAsync(buffer, 0, buffer.Length)) > 0) + { + await fout.WriteAsync(buffer, 0, count); + + progress += count; + } + } + } + } + catch (Exception) + { + throw new FFmpegNotDownloadedException(); + } + + return true; + } + + /// + /// Set the download url for ffmpeg library + /// + /// + public void SetDownloadUrl(string url) + { + Url = url; + } + + /// + /// Set the source folder containing the ffmpeg library + /// + /// + public void SetSourceFolder(string path) + { + SourceFolder = path; + } + + /// + /// Set the source folder containing the ffmpeg library + /// + /// + public void SetDownloadTitle(string title) + { + DownloadTitle = title; + } + } +} \ No newline at end of file diff --git a/Xamarin.FFmpeg.Android/Init.cs b/Xamarin.FFmpeg.Android/Init.cs new file mode 100644 index 0000000..85bcfad --- /dev/null +++ b/Xamarin.FFmpeg.Android/Init.cs @@ -0,0 +1,15 @@ +using Android.Content; + +namespace Xamarin.FFmpeg.Android +{ + public class Init + { + public static void Initialize(Context context) + { + FFmpeg.Init.Initialize(new FFmpegLibrary + { + Context = context + }); + } + } +} \ No newline at end of file diff --git a/Xamarin.FFmpeg/Properties/AssemblyInfo.cs b/Xamarin.FFmpeg.Android/Properties/AssemblyInfo.cs similarity index 100% rename from Xamarin.FFmpeg/Properties/AssemblyInfo.cs rename to Xamarin.FFmpeg.Android/Properties/AssemblyInfo.cs diff --git a/Xamarin.FFmpeg/Resources/AboutResources.txt b/Xamarin.FFmpeg.Android/Resources/AboutResources.txt similarity index 100% rename from Xamarin.FFmpeg/Resources/AboutResources.txt rename to Xamarin.FFmpeg.Android/Resources/AboutResources.txt diff --git a/Xamarin.FFmpeg/Resources/Resource.Designer.cs b/Xamarin.FFmpeg.Android/Resources/Resource.Designer.cs similarity index 93% rename from Xamarin.FFmpeg/Resources/Resource.Designer.cs rename to Xamarin.FFmpeg.Android/Resources/Resource.Designer.cs index 8758e70..26a73f0 100644 --- a/Xamarin.FFmpeg/Resources/Resource.Designer.cs +++ b/Xamarin.FFmpeg.Android/Resources/Resource.Designer.cs @@ -9,9 +9,9 @@ // //------------------------------------------------------------------------------ -[assembly: global::Android.Runtime.ResourceDesignerAttribute("Xamarin.FFmpeg.Resource", IsApplication=false)] +[assembly: global::Android.Runtime.ResourceDesignerAttribute("Xamarin.FFmpeg.Android.Resource", IsApplication=false)] -namespace Xamarin.FFmpeg +namespace Xamarin.FFmpeg.Android { diff --git a/Xamarin.FFmpeg/Resources/Values/Strings.xml b/Xamarin.FFmpeg.Android/Resources/Values/Strings.xml similarity index 100% rename from Xamarin.FFmpeg/Resources/Values/Strings.xml rename to Xamarin.FFmpeg.Android/Resources/Values/Strings.xml diff --git a/Xamarin.FFmpeg/FFMpeg.Xamarin.csproj.bak b/Xamarin.FFmpeg.Android/Xamarin.FFmpeg.Android.csproj similarity index 83% rename from Xamarin.FFmpeg/FFMpeg.Xamarin.csproj.bak rename to Xamarin.FFmpeg.Android/Xamarin.FFmpeg.Android.csproj index a9554b0..051e372 100644 --- a/Xamarin.FFmpeg/FFMpeg.Xamarin.csproj.bak +++ b/Xamarin.FFmpeg.Android/Xamarin.FFmpeg.Android.csproj @@ -9,13 +9,12 @@ {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} Library Properties - FFMpeg.Xamarin - FFMpeg.Xamarin + Xamarin.FFmpeg.Android + Xamarin.FFmpeg.Android 512 Resources\Resource.Designer.cs Off - True - v7.1 + v9.0 true @@ -44,8 +43,9 @@ - - + + + @@ -55,6 +55,12 @@ + + + {807f181e-43aa-4516-a86d-d68bb9ef5e95} + Xamarin.FFmpeg + + - \ No newline at end of file + + diff --git a/XamarinAndroidFFmpeg.sln b/XamarinAndroidFFmpeg.sln index 1ec037b..94e9ec1 100644 --- a/XamarinAndroidFFmpeg.sln +++ b/XamarinAndroidFFmpeg.sln @@ -1,16 +1,16 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27130.2020 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29201.188 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Xamarin.FFmpeg.Nuget", "Xamarin.FFmpeg.Nuget\Xamarin.FFmpeg.Nuget.csproj", "{0451BAEF-DF2E-4B98-8644-94EE9415E389}" ProjectSection(ProjectDependencies) = postProject {1049745B-3D4C-4DFF-AD8B-DC46F460B5D4} = {1049745B-3D4C-4DFF-AD8B-DC46F460B5D4} EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Xamarin.FFmpeg", "Xamarin.FFmpeg\Xamarin.FFmpeg.csproj", "{1049745B-3D4C-4DFF-AD8B-DC46F460B5D4}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Xamarin.FFmpeg.Android", "Xamarin.FFmpeg.Android\Xamarin.FFmpeg.Android.csproj", "{1049745B-3D4C-4DFF-AD8B-DC46F460B5D4}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A2C28ED0-0741-4CDE-B9FA-604C145DE4D9}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Xamarin.FFmpeg", "Xamarin.FFMpeg\Xamarin.FFmpeg.csproj", "{807F181E-43AA-4516-A86D-D68BB9EF5E95}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -26,6 +26,10 @@ Global {1049745B-3D4C-4DFF-AD8B-DC46F460B5D4}.Debug|Any CPU.Build.0 = Debug|Any CPU {1049745B-3D4C-4DFF-AD8B-DC46F460B5D4}.Release|Any CPU.ActiveCfg = Release|Any CPU {1049745B-3D4C-4DFF-AD8B-DC46F460B5D4}.Release|Any CPU.Build.0 = Release|Any CPU + {807F181E-43AA-4516-A86D-D68BB9EF5E95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {807F181E-43AA-4516-A86D-D68BB9EF5E95}.Debug|Any CPU.Build.0 = Debug|Any CPU + {807F181E-43AA-4516-A86D-D68BB9EF5E95}.Release|Any CPU.ActiveCfg = Release|Any CPU + {807F181E-43AA-4516-A86D-D68BB9EF5E95}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Additions/AboutAdditions.txt b/src/Additions/AboutAdditions.txt deleted file mode 100644 index 08caee3..0000000 --- a/src/Additions/AboutAdditions.txt +++ /dev/null @@ -1,48 +0,0 @@ -Additions allow you to add arbitrary C# to the generated classes -before they are compiled. This can be helpful for providing convenience -methods or adding pure C# classes. - -== Adding Methods to Generated Classes == - -Let's say the library being bound has a Rectangle class with a constructor -that takes an x and y position, and a width and length size. It will look like -this: - -public partial class Rectangle -{ - public Rectangle (int x, int y, int width, int height) - { - // JNI bindings - } -} - -Imagine we want to add a constructor to this class that takes a Point and -Size structure instead of 4 ints. We can add a new file called Rectangle.cs -with a partial class containing our new method: - -public partial class Rectangle -{ - public Rectangle (Point location, Size size) : - this (location.X, location.Y, size.Width, size.Height) - { - } -} - -At compile time, the additions class will be added to the generated class -and the final assembly will a Rectangle class with both constructors. - - -== Adding C# Classes == - -Another thing that can be done is adding fully C# managed classes to the -generated library. In the above example, let's assume that there isn't a -Point class available in Java or our library. The one we create doesn't need -to interact with Java, so we'll create it like a normal class in C#. - -By adding a Point.cs file with this class, it will end up in the binding library: - -public class Point -{ - public int X { get; set; } - public int Y { get; set; } -} \ No newline at end of file diff --git a/src/Jars/AboutJars.txt b/src/Jars/AboutJars.txt deleted file mode 100644 index c359b62..0000000 --- a/src/Jars/AboutJars.txt +++ /dev/null @@ -1,24 +0,0 @@ -This directory is for Android .jars. - -There are 2 types of jars that are supported: - -== Input Jar == - -This is the jar that bindings should be generated for. - -For example, if you were binding the Google Maps library, this would -be Google's "maps.jar". - -Set the build action for these jars in the properties page to "InputJar". - - -== Reference Jars == - -These are jars that are referenced by the input jar. C# bindings will -not be created for these jars. These jars will be used to resolve -types used by the input jar. - -NOTE: Do not add "android.jar" as a reference jar. It will be added automatically -based on the Target Framework selected. - -Set the build action for these jars in the properties page to "ReferenceJar". \ No newline at end of file diff --git a/src/Jars/FFmpegAndroid-debug.aar b/src/Jars/FFmpegAndroid-debug.aar deleted file mode 100644 index c78b1eb..0000000 Binary files a/src/Jars/FFmpegAndroid-debug.aar and /dev/null differ diff --git a/src/Properties/AssemblyInfo.cs b/src/Properties/AssemblyInfo.cs deleted file mode 100644 index 6715424..0000000 --- a/src/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using Android.App; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Audition800.FFmpeg")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Audition800.FFmpeg")] -[assembly: AssemblyCopyright("Copyright © 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] -[assembly: ComVisible(false)] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Transforms/EnumFields.xml b/src/Transforms/EnumFields.xml deleted file mode 100644 index 2295995..0000000 --- a/src/Transforms/EnumFields.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - \ No newline at end of file diff --git a/src/Transforms/EnumMethods.xml b/src/Transforms/EnumMethods.xml deleted file mode 100644 index 49216c6..0000000 --- a/src/Transforms/EnumMethods.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - \ No newline at end of file diff --git a/src/Transforms/Metadata.xml b/src/Transforms/Metadata.xml deleted file mode 100644 index 75aebe4..0000000 --- a/src/Transforms/Metadata.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - public - public - - - diff --git a/src/Xamarin.FFmpeg.csproj b/src/Xamarin.FFmpeg.csproj deleted file mode 100644 index ea70612..0000000 --- a/src/Xamarin.FFmpeg.csproj +++ /dev/null @@ -1,61 +0,0 @@ - - - - Debug - AnyCPU - 8.0.30703 - 2.0 - {430983C2-C8A0-4644-91E0-397CD6E43063} - {10368E6C-D01B-4462-8E8B-01FC667A7035};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - Library - Properties - Audition800.FFmpeg - Audition800.FFmpeg - 512 - True - v6.0 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file