You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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,运行时复制到可执行目录并设置权限。

四、音视频文件存储最佳实践

  1. 应用私有目录

    • 适用场景:临时转换文件、无需用户访问的缓存数据。
    • Android:Context.FilesDir(永久存储)、Context.CacheDir(可被系统清理)。
    • iOS/MacCatalyst:Environment.SpecialFolder.MyDocuments(永久,会iCloud同步)、Environment.SpecialFolder.Caches(可清理)。
    • Windows:Environment.SpecialFolder.LocalApplicationData。
  2. 共享存储

    • 适用场景:需要用户在系统相册/文件管理器中访问的成品文件。
    • Android:通过MediaStore插入到Movies/Music目录,避免直接操作外部存储根目录。
    • iOS:使用PHPhotoLibrary将文件保存到系统相册,需在Info.plist添加NSPhotoLibraryAddUsageDescription权限描述。
    • Windows:保存到Environment.SpecialFolder.MyVideos目录。
  3. 注意事项

    • 定期清理缓存目录,避免占用过多存储空间。
    • Android 13+需申请READ_MEDIA_VIDEO/READ_MEDIA_AUDIO权限访问共享媒体。

内容的提问来源于stack exchange,提问作者Mrand

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 01:19:53