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

Android 10与11下Flutter FFmpeg存储路径兼容问题求助

Android 10/11存储路径适配问题:FFmpeg输出路径版本兼容故障排查

问题背景

我修改了FFmpeg的输出路径,指定为硬编码的应用专属外部存储路径:

String outputPath = '/storage/emulated/0/Android/data/com.example.videocutter/files/MyFolder/$formattedDateTime.mp4';

该实现在Android 10物理设备上运行正常,但在Android 11模拟器测试时抛出异常。

当改为公共外部存储路径后:

final String outputPath = '${directory.path}/download/$outputFileName';

Android 11的问题解决了,但Android 10上又出现类似权限/路径错误。需要分析版本差异根因,并给出通用解决方案。

原始代码

Future<void> trimVideos() async {
  try {
    final appDocDir = await getApplicationDocumentsDirectory();
    String formattedDateTime = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now());
    //String outputPath = '${appDocDir.path}/$formattedDateTime.mp4';
    String outputPath = '/storage/emulated/0/Android/data/com.example.videocutter/files/MyFolder/$formattedDateTime.mp4';
    final String inputPath=widget.file.path;
    final Duration start = _controller.startTrim;
    final Duration end = _controller.endTrim;
    // Build the FFmpeg command for trimming with keyframe alignment

    final command = '-ss ${formatter(start)} -t ${formatter(end - start)} -noaccurate_seek -i $inputPath -codec copy -avoid_negative_ts 1 $outputPath';
    // Execute FFmpeg command
    await FFmpegKit.execute(command).then((session) async {
      print('The command is executed: $command');
      final returnCode = await session.getReturnCode();
      if (ReturnCode.isSuccess(returnCode)) {
        print('Command is executed successfully');
        setState(() {

        });
      }

      else if (ReturnCode.isCancel(returnCode)) {
        print('Command is canceled');
      } else {
        print('Error $returnCode');
        final logs = await session.getLogsAsString();
        print('Error details: $logs');
      }
    });
  } catch (e) {
    print('Error trimming video: $e');
  }
}

错误日志

Loaded ffmpeg-kit-flutter-android-https-x86-5.1.0.
I/flutter (15789): The command is executed: -ss 00:00 -t 01:00 -noaccurate_seek -i /data/user/0/com.example.videocutter/cache/86eb14d4-041d-4c7a-a719-3394b318af56/Mercy - Badshah Feat. Lauren Gottlieb _ Official Music Video _ Latest Hit Song 2017.mp4 -codec copy -avoid_negative_ts 1 /storage/emulated/0/Android/data/com.example.videocutter/files/MyFolder/20231121_152801.mp4
I/flutter (15789): Error 1
I/flutter (15789): Error details: ffmpeg version n5.1.2 Copyright (c) 2000-2022 the FFmpeg developers
I/flutter (15789): built with Android (7155654, based on r399163b1) clang version 11.0.5 (https://android.googlesource.com/toolchain/llvm-project 87f1315dfbea7c137aa2e6d362dbb457e388158d)
I/flutter (15789): configuration: --cross-prefix=i686-linux-android- --sysroot=/files/android-sdk/ndk/22.1.7171670/toolchains/llvm/prebuilt/linux-x86_64/sysroot --prefix=/home/taner/Projects/ffmpeg-kit/prebuilt/android-x86/ffmpeg --pkg-config=/usr/bin/pkg-config --enable-version3 --arch=i686 --cpu=i686 --target-os=android --disable-neon --disable-asm --disable-inline-asm --ar=i686-linux-android-ar --cc=i686-linux-android24-clang --cxx=i686-linux-android24-clang++ --ranlib=i686-linux-android-ranlib --strip=i686-linux-android-strip --nm=i686-linux-android-nm --extra-libs='-L/home/taner/Projects/ffmpeg-kit/prebuilt/android-x86/cpu-features/lib -lndk_compat' --disable-autodetect --enable-cross-compile --enable-pic --enable-jni --enable-optimizations --enable-swscale --disable-sta

根因分析

Android 10(API 29)特性

  1. 分区存储默认启用,但支持通过android:requestLegacyExternalStorage="true"在Manifest中兼容旧模式,允许直接访问硬编码路径。
  2. 应用专属外部存储(/Android/data/包名/files/)属于应用私有空间,无需额外权限即可访问,因此硬编码路径能正常工作。
  3. 访问公共存储目录(如Download)需要申请WRITE_EXTERNAL_STORAGE权限,否则会因权限不足报错。

Android 11(API 30)特性

  1. 分区存储强制启用,requestLegacyExternalStorage不再生效,系统对路径权限校验更严格。
  2. 硬编码应用专属外部存储路径可能失效:FFmpeg作为Native进程,直接访问硬编码路径时,系统可能拒绝其权限(正确方式是通过Android API获取路径,而非硬编码)。
  3. 公共存储目录访问逻辑变更,直接通过路径写入需要符合分区存储规则,部分场景下需使用MediaStore API。

额外问题:路径含特殊字符未转义

从错误日志中的FFmpeg命令可见,输入路径包含空格,未用引号包裹,这会导致FFmpeg解析路径失败,也是触发错误的可能原因之一。

解决方案

方案1:使用应用专属外部存储(推荐,无需额外权限)

通过Flutter官方API获取路径,避免硬编码,确保跨版本兼容:

Future<void> trimVideos() async {
  try {
    final externalDir = await getExternalStorageDirectory();
    // 创建自定义文件夹并确保存在
    final myFolder = Directory('${externalDir.path}/MyFolder');
    if (!await myFolder.exists()) {
      await myFolder.create(recursive: true);
    }
    String formattedDateTime = DateFormat('yyyyMMdd_HHmmss').format(DateTime.now());
    String outputPath = '${myFolder.path}/$formattedDateTime.mp4';
    final String inputPath = widget.file.path;
    final Duration start = _controller.startTrim;
    final Duration end = _controller.endTrim;

    // 路径含特殊字符需用引号包裹
    final command = '-ss ${formatter(start)} -t ${formatter(end - start)} -noaccurate_seek -i "$inputPath" -codec copy -avoid_negative_ts 1 "$outputPath"';
    
    await FFmpegKit.execute(command).then((session) async {
      // 后续逻辑保持不变...
    });
  } catch (e) {
    print('Error trimming video: $e');
  }
}
  • 该路径在Android 10/11均属于应用私有空间,无需申请存储权限。
  • 通过create(recursive: true)确保目标目录存在,避免FFmpeg因目录不存在报错。

方案2:输出到公共存储目录(如Download)

Android 10及以下

  1. 在AndroidManifest.xml中声明权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application android:requestLegacyExternalStorage="true">
  1. 动态申请存储权限:
if (await Permission.storage.request().isGranted) {
  // 执行写入逻辑
}

Android 11及以上

使用MediaStore API写入公共目录(推荐使用media_store插件简化操作),示例代码:

import 'package:media_store/media_store.dart';

Future<String> getPublicDownloadPath(String fileName) async {
  if (Platform.isAndroid) {
    final mediaStore = MediaStore();
    final uri = await mediaStore.createFile(
      path: MediaStore.downloads,
      mimeType: 'video/mp4',
      displayName: fileName,
    );
    return uri.path;
  }
  return '';
}
  • 此方式无需申请WRITE_EXTERNAL_STORAGE权限,符合Android 11+分区存储规则。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 14:47:03