MAUI Blazor项目Android平台FFmpeg使用问题及存储方案咨询
MAUI Blazor + FFmpeg Android 问题修复与多平台实现方案
一、Android平台核心问题修复
1. 解决Xabe.FFmpeg「找不到路径」问题
你的PrepareFFmpeg方法存在关键错误:FFmpeg.SetExecutablesPath需要传入FFmpeg二进制文件所在的目录,而非单个文件路径。当前代码将destinationPath指向具体的ffmpeg文件,导致Xabe库无法定位工具集。
修改后的PrepareFFmpeg代码:
private void PrepareFFmpeg() { var assetManager = Android.App.Application.Context.Assets; string assetFilePath = "libs/ffmpeg"; // 创建存放FFmpeg的目录(而非单个文件路径) string ffmpegDir = Path.Combine(Android.App.Application.Context.ApplicationInfo.DataDir, "ffmpeg"); string destinationFilePath = Path.Combine(ffmpegDir, "ffmpeg"); // 确保目录存在 if (!Directory.Exists(ffmpegDir)) { Directory.CreateDirectory(ffmpegDir); } // 仅当文件不存在时复制,避免重复操作 if (!File.Exists(destinationFilePath)) { using (var inputStream = assetManager.Open(assetFilePath)) using (var outputStream = File.Create(destinationFilePath)) { inputStream.CopyTo(outputStream); } } // 可靠设置执行权限(替代Runtime.Exec,避免环境变量问题) var fileInfo = new FileInfo(destinationFilePath); fileInfo.UnixFileMode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute; // 设置Xabe.FFmpeg的工具目录 FFmpeg.SetExecutablesPath(ffmpegDir); }
2. 解决「权限拒绝」问题
Android 10+启用了Scoped Storage,传统的ReadExternalStorage/WriteExternalStorage权限已无法直接访问共享存储文件,需调整以下两点:
- 权限适配:针对Android 13+,替换为
READ_MEDIA_VIDEO权限;在AndroidManifest.xml中添加:<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <!-- 兼容Android 12及以下 --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" /> - 文件路径处理:
- 输入文件:若用户从系统相册选择,需通过
ContentResolver将文件复制到应用私有目录(如Context.CacheDir)后再传入FFmpeg,因为FFmpeg无法直接访问Content URI。 - 输出文件:优先使用应用私有目录(如
Context.FilesDir或Context.CacheDir),若需让用户访问,通过MediaStore将文件插入到共享媒体库。
- 输入文件:若用户从系统相册选择,需通过
二、FFmpegService 优化(跨平台兼容)
修改FFmpegService,完善Android逻辑并添加基础跨平台支持:
public static class FFmpegService { public static async Task ConvertVideoAsync(string inputPath, string outputPath, string format) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { string ffmpegPath = GetFFmpegPathForWindows(); string arguments = $"-i \"{inputPath}\" \"{outputPath}.{format}\""; ProcessStartInfo startInfo = new ProcessStartInfo { FileName = ffmpegPath, Arguments = arguments, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, }; using Process process = new Process { StartInfo = startInfo }; process.Start(); await process.WaitForExitAsync(); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.Android)) { string outputPathWithFormat = $"{outputPath}.{format}"; // 确保输入路径是应用可访问的文件路径(非Content URI) if (inputPath.StartsWith("content://")) { inputPath = await CopyContentUriToPrivatePath(inputPath); } IConversion conversion = await FFmpeg.Conversions.FromSnippet.Convert(inputPath, outputPathWithFormat); // 添加日志监听,便于调试 conversion.OnProgress += (sender, args) => Console.WriteLine($"进度:{args.Percent}%"); conversion.OnError += (sender, args) => Console.WriteLine($"错误:{args.Message}"); IConversionResult result = await conversion.Start(); // 若需将输出文件共享到系统相册 if (result.Success) { await AddFileToMediaStore(outputPathWithFormat); } } // iOS/MacCatalyst 示例逻辑 else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.iOS)) { string ffmpegDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "ffmpeg"); string ffmpegPath = Path.Combine(ffmpegDir, "ffmpeg"); // 提前将FFmpeg二进制从Resources复制到Documents目录 EnsureFFmpegInPlace(ffmpegPath); string arguments = $"-i \"{inputPath}\" \"{outputPath}.{format}\""; ProcessStartInfo startInfo = new ProcessStartInfo { FileName = ffmpegPath, Arguments = arguments, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, }; using Process process = new Process { StartInfo = startInfo }; process.Start(); await process.WaitForExitAsync(); } } private static string GetFFmpegPathForWindows() { return Path.Combine(AppContext.BaseDirectory, "Platforms", "Windows", "ffmpeg", "ffmpeg.exe"); } // Android:将Content URI复制到应用私有缓存目录 private static async Task<string> CopyContentUriToPrivatePath(string contentUri) { var context = Android.App.Application.Context; var cacheDir = context.CacheDir; var fileName = Guid.NewGuid().ToString() + ".mp4"; // 根据实际格式调整 var destPath = Path.Combine(cacheDir.AbsolutePath, fileName); using (var inputStream = context.ContentResolver.OpenInputStream(Android.Net.Uri.Parse(contentUri))) using (var outputStream = File.Create(destPath)) { await inputStream.CopyToAsync(outputStream); } return destPath; } // Android:将输出文件添加到系统媒体库 private static async Task AddFileToMediaStore(string filePath) { var context = Android.App.Application.Context; var values = new ContentValues(); values.Put(MediaStore.Video.Media.Columns.DisplayName, Path.GetFileName(filePath)); values.Put(MediaStore.Video.Media.Columns.MimeType, "video/" + Path.GetExtension(filePath).Trim('.')); values.Put(MediaStore.Video.Media.Columns.RelativePath, "Movies/YourAppName"); var uri = context.ContentResolver.Insert(MediaStore.Video.Media.ExternalContentUri, values); using (var outputStream = context.ContentResolver.OpenOutputStream(uri)) using (var inputStream = File.OpenRead(filePath)) { await inputStream.CopyToAsync(outputStream); } } // iOS/MacCatalyst:确保FFmpeg二进制存在 private static void EnsureFFmpegInPlace(string ffmpegPath) { if (File.Exists(ffmpegPath)) return; var dir = Path.GetDirectoryName(ffmpegPath); if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); // 从Resources复制FFmpeg(需将iOS的FFmpeg二进制添加到Resources并设置Build Action为BundleResource) using (var inputStream = typeof(FFmpegService).Assembly.GetManifestResourceStream("YourAppName.Resources.ffmpeg")) using (var outputStream = File.Create(ffmpegPath)) { inputStream.CopyTo(outputStream); } // 设置执行权限 var fileInfo = new FileInfo(ffmpegPath); fileInfo.UnixFileMode = UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute; } }
三、跨平台FFmpeg部署要点
- Windows:将FFmpeg二进制包放到
Platforms/Windows/ffmpeg目录,设置Build Action为Content,确保发布时包含。 - Android:编译对应ABI的FFmpeg二进制(建议使用预编译包如
mobile-ffmpeg),放到Platforms/Android/Assets/libs,设置Build Action为AndroidAsset。 - iOS/MacCatalyst:下载对应架构的FFmpeg二进制,添加到项目Resources目录,设置Build Action为
BundleResource。 - Linux:可依赖系统自带的FFmpeg,或打包二进制到项目中,设置Build Action为
Content,运行时复制到可执行目录并设置权限。
四、音视频文件存储最佳实践
应用私有目录
- 适用场景:临时转换文件、无需用户访问的缓存数据。
- Android:
Context.FilesDir(永久存储)、Context.CacheDir(可被系统清理)。 - iOS/MacCatalyst:
Environment.SpecialFolder.MyDocuments(永久,会iCloud同步)、Environment.SpecialFolder.Caches(可清理)。 - Windows:
Environment.SpecialFolder.LocalApplicationData。
共享存储
- 适用场景:需要用户在系统相册/文件管理器中访问的成品文件。
- Android:通过
MediaStore插入到Movies/Music目录,避免直接操作外部存储根目录。 - iOS:使用
PHPhotoLibrary将文件保存到系统相册,需在Info.plist添加NSPhotoLibraryAddUsageDescription权限描述。 - Windows:保存到
Environment.SpecialFolder.MyVideos目录。
注意事项
- 定期清理缓存目录,避免占用过多存储空间。
- Android 13+需申请
READ_MEDIA_VIDEO/READ_MEDIA_AUDIO权限访问共享媒体。
内容的提问来源于stack exchange,提问作者Mrand
相关产品推荐
相关产品推荐

