likes
comments
collection
share

Flutter-Android-APP实现音频进行提取并上传s3服务器

作者站长头像
站长
· 阅读数 65

Flutter 框架本身没有提供原生文件选择器的功能。但是,你可以使用第三方插件来实现在 Flutter 应用中调用本地文件选择器的功能。

file_picker插件实现

file_picker 是一个 Flutter 插件,用于在应用程序中实现文件选择功能。它支持安卓和 iOS 平台,让开发者可以方便地让用户选择需要上传或处理的文件,提高应用程序的灵活性和功能性。

官网单文件使用案例

Widget _buildBody() {
    return Material(
      child: Row(
        children: <Widget>[
          TextButton(
            onPressed: () async {
              FilePickerResult? result = await FilePicker.platform.pickFiles();

              if (result != null) {
                File file = File(result.files.single.path!);
              } else {
                // User canceled the picker
              }
            },
            child: Text(
              '本地文件转写',
              style: TextStyle(
                  fontSize: 20,
                  fontWeight: FontWeight.bold,
                  color: Colors.black),
            ),
          )
        ],
      ),
    );
  }

安卓使用file_pick打开本地选择器问题

D/EGL_emulation( 6791): app_time_stats: avg=43755.50ms min=3.59ms max=699872.19ms count=16
D/EGL_emulation( 6791): app_time_stats: avg=5560.07ms min=6.79ms max=188490.08ms count=34
I/flutter ( 6791): [MethodChannelFilePicker] Platform exception: PlatformException(read_external_storage_denied, User did not allow reading external storage, null, null)
E/flutter ( 6791): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: PlatformException(read_external_storage_denied, User did not allow reading external storage, null, null)
E/flutter ( 6791): #0      StandardMethodCodec.decodeEnvelope (package:flutter/src/services/message_codecs.dart:651:7)
E/flutter ( 6791): #1      MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:334:18)
E/flutter ( 6791): <asynchronous suspension>
E/flutter ( 6791): #2      MethodChannel.invokeListMethod (package:flutter/src/services/platform_channel.dart:520:35)
E/flutter ( 6791): <asynchronous suspension>
E/flutter ( 6791): #3      FilePickerIO._getPath (package:file_picker/src/file_picker_io.dart:93:33)
E/flutter ( 6791): <asynchronous suspension>
E/flutter ( 6791): #4      _HomeScreenState._buildBody.<anonymous closure> (package:boilerplate/presentation/home/home.dart:40:43)
E/flutter ( 6791): <asynchronous suspension>
E/flutter ( 6791): 

这个错误提示表明用户没有允许应用程序读取外部存储空间的权限。在 Android 上,需要确保在 AndroidManifest.xml 文件中请求相关权限

允许权限 android -> app -> src -> main -> AndroidManifest.xml 
    <!-- 写权限 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <!-- 读权限 -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

这样就可以打开本地文件选择器ok

Flutter-Android-APP实现音频进行提取并上传s3服务器

那么IOS上如何使用file_picker?

在 iOS 上使用 file_picker 并不会涉及到外部存储空间的权限,因为 iOS 并没有像 Android 那样的外部存储概念。在 iOS 上,文件访问受到应用沙盒的限制,每个应用只能访问自己的沙盒内部的文件。

因此,如果你遇到了关于外部存储空间权限的报错,很可能是在 Android 平台上出现的情况。在 iOS 上使用 file_picker 时,你需要确保应用有适当的权限来访问相册或 iCloud Drive 等位置。你可以通过在 Info.plist 文件中添加相应的权限描述来请求这些权限。

例如,如果你想要访问相册,可以在 Info.plist 中添加类似以下的权限描述:

ios -> Runner -> info.plist 添加访问时的权限提示

    <key>NSPhotoLibraryAddUsageDescription</key>
    <string>App需要您的同意,才能访问相册</string>
    <key>NSPhotoLibraryUsageDescription</key>
    <string>App需要您的同意,才能访问相册</string>
    <key>NSFileProviderExtensionPointIdentifier</key>
    <string>com.apple.fileprovider-nonui</string>
    <key>NSDocumentsFolderUsageDescription</key>
    <string>App需要您的同意,才能访问文件</string>
    <key>NSDownloadsFolderUsageDescription</key>
    <string>App需要您的同意,才能访问下载文件</string>

这样,在使用 file_picker 时,系统会向用户请求相应的权限,并在用户授权后,你的应用程序就可以访问相应的文件了。

在依次针对安卓问题进行解决

随后发现读取选取本地文件音频视频 不需要请求存储权限 随后 就去除了permission_handler这一步 用户点击‘ 选取本地文件 直接打开文件选择器 ’

实现: 点击打开本地文件选择器

第一步: 允许权限 android -> app -> src -> main -> AndroidManifest.xml 
    <!-- 写权限 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <!-- 读权限 -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
第二步:使用file_pick选取文件
Widget _buildBody() {
    return Material(
      child: Row(
        children: <Widget>[
          TextButton(
            onPressed: () async {
              // 用户已授权,弹出文件选择器
              FilePickerResult? result = await FilePicker.platform.pickFiles();
              if (result != null) {
                // 用户选择了文件
                File file = File(result.files.single.path!);
                // 进行其他操作...
              } else {
                // 用户取消了文件选择
              }
            },
            child: Text(
              '本地文件转写',
              style: TextStyle(
                  fontSize: 20,
                  fontWeight: FontWeight.bold,
                  color: Colors.black),
            ),
          )
        ],
      ),
    );
  }

实现对视频的音频进行提取

指定允许选择的文件扩展名
FilePickerResult? result = await FilePicker.platform.pickFiles();
if (result != null) {
  // 用户选择了文件
  String? path = result.files.single.path;
  if (path != null) {
    File file = File(path);
    print('file: $file');
    // 进行其他操作...
  } else {
    // 文件路径为空,可能是由于用户取消选择
  }
} else {
  // 用户取消了文件选择
}

下一步实现的是对视频的音频提取 使用 : 音视频处理库ffmpeg_kit_flutte

  1. 转码视频文件

    '-i input.mp4 -c:v libx264 -c:a aac output.mp4'
    
    
  2. 裁剪视频

    '-i input.mp4 -ss 00:00:10 -t 00:00:05 -c:v copy -c:a copy output.mp4'
    
    
  3. 提取音频

    '-i input.mp4 -vn -acodec copy output.m4a'
    
  4. 合并音视频

    '-i input.mp4 -i input.mp3 -c:v copy -c:a aac -strict experimental output.mp4'
    
    
  5. 改变分辨率

    '-i input.mp4 -vf scale=640:360 output.mp4'
    
    
  6. 改变帧率

    '-i input.mp4 -r 24 output.mp4'
    
  7. 添加水印

    '-i input.mp4 -i watermark.png -filter_complex "overlay=10:10" output.mp4'
    
    
  8. 提取关键帧

    '-i input.mp4 -vf select="eq(pict_type,I)" -vsync vfr output-%03d.jpg'
    

实现抽取音频

 // 提取音频函数
  Future<void> extractAudio(File file) async {
    try {
      // 获取音频文件信息
      MediaInformationSession? mediaInformationSession =
          await FFprobeKit.getMediaInformation(file.path);

      if (mediaInformationSession != null &&
          mediaInformationSession.getMediaInformation() != null) {
        MediaInformation mediaInformation =
            mediaInformationSession.getMediaInformation()!;
        // 检查是否包含音频流
        if (mediaInformation
            .getStreams()
            .any((stream) => stream.getType() == 'audio')) {
          // 设置输出音频文件路径
          String outputAudioPath = '${file.path}.aac';
          print('filepath:$outputAudioPath');
          // 执行 FFmpeg 命令,获取音频流的信息
          FFmpegSession result = await FFmpegKit.execute(
              '-i ${file.path} -y -vn -c copy ${outputAudioPath}');
          // 等待会话执行完成
          await result;
          // 获取返回码
          ReturnCode? returnCode = await result.getReturnCode();
          var ResultLog = await result.getAllLogsAsString();
          print('ResultLog:$ResultLog');
          print('Return Code: $returnCode');
          if (ReturnCode.isSuccess(returnCode)) {
            print('音频抽取成功');
          } else {
            print('FFmpeg 命令执行失败');
          }
        } else {
          print('文件不包含音频流');
          // Fluttertoast.showToast(msg: '文件不包含音频流');
        }
      } else {
        print('无法获取文件信息');
        // Fluttertoast.showToast(msg: '无法获取文件信息');
      }
    } catch (e) {
      print('音频提取失败,错误: $e');
      // Fluttertoast.showToast(msg: '音频提取失败,错误: $e');
    }
  }

抽取的音频存储在了虚拟机的缓存目录中

Flutter-Android-APP实现音频进行提取并上传s3服务器 使用aws_s3_api: ^2.0.0 将抽取的音频文件上传到Amazon S3 服务器

// 将 Stream<List<int>> 转换为 Uint8List
  Future<Uint8List> streamToUint8List(Stream<List<int>> stream) async {
    var bytes = <int>[];
    await for (var chunk in stream) {
      bytes.addAll(chunk);
    }
    return Uint8List.fromList(bytes);
  }

Future<void> uploadAudioFile(String mediaURL, String filePath, num? duration,
      String? filename, num? fileSize, String userId) async {
    bool succeeded = true;
    String assetId = '';

    Map<String, dynamic> data = {
      'SessionID': userId,
      'mediaURL': mediaURL,
      'duration': duration,
      'metadata': '',
      'filename': '3.m4a',
      'fileSize': fileSize,
    };

    String jsonData = jsonEncode(data);

    final res = await dioClient.dio
        .post('api/扫描音频', data: jsonData);

    dynamic responseData = res.data;
    if (responseData['head']['code'] == 0 && responseData['data'] != null) {
      assetId = responseData['data']['assetID'];
    }

    String desc = '';
    try {
        // 使用 streamToUint8List 方法将 fileStream 转换为 Uint8List
      final s3 = S3(
          region: 'S3存储桶所在的AWS区域',
          credentials: AwsClientCredentials(
                accessKey: 'AWS S3的访问密钥',
                secretKey: 'AWS S3的秘密密钥'));
      final bucket = '要上传到的S3存储桶的名称';
      final file = File(filePath);
      Uint8List fileBytes = await streamToUint8List(file.openRead());

      s3.putObject(
          bucket: bucket,
          key: mediaURL,
          body: fileBytes,
          contentLength: fileBytes.length);

    } catch (err) {
      succeeded = false;
      desc = err.toString();
    } finally {
      await dioClient.dio.post('/api/上传完成', data: {
        'assetID': assetId,
        'status': succeeded ? 100 : 50,
        'desc': desc,
      });
    }
  }

完整代码

Widget _buildBody() {
    return Material(
      child: Row(
        children: <Widget>[
          TextButton(
            onPressed: () async {
              // 用户已授权,弹出文件选择器
              FilePickerResult? result = await FilePicker.platform.pickFiles();
              if (result != null) {
                // 用户选择了文件
                String? path = result.files.single.path;
                if (path != null) {
                  File file = File(path);
                  print('file: $file');
                  // 提取音频
                  await extractAudio(file);
                } else {
                  // 文件路径为空,可能是由于用户取消选择
                }
              } else {
                // 用户取消了文件选择
              }
            },
            child: Text(
              '本地文件转写',
              style: TextStyle(
                  fontSize: 20,
                  fontWeight: FontWeight.bold,
                  color: Colors.black),
            ),
          )
        ],
      ),
    );
  }

// 将 Stream<List<int>> 转换为 Uint8List
  Future<Uint8List> streamToUint8List(Stream<List<int>> stream) async {
    var bytes = <int>[];
    await for (var chunk in stream) {
      bytes.addAll(chunk);
    }
    return Uint8List.fromList(bytes);
  }

Future<void> uploadAudioFile(String mediaURL, String filePath, num? duration,
      String? filename, num? fileSize, String userId) async {
    bool succeeded = true;
    String assetId = '';

    Map<String, dynamic> data = {
      'SessionID': userId,
      'mediaURL': mediaURL,
      'duration': duration,
      'metadata': '',
      'filename': '3.m4a',
      'fileSize': fileSize,
    };

    String jsonData = jsonEncode(data);

    final res = await dioClient.dio
        .post('api/扫描音频', data: jsonData);

    dynamic responseData = res.data;
    if (responseData['head']['code'] == 0 && responseData['data'] != null) {
      assetId = responseData['data']['assetID'];
    }

    String desc = '';
    try {
       // 使用 streamToUint8List 方法将 fileStream 转换为 Uint8List
      final s3 = S3(
          region: 'xxxxx',
          credentials: AwsClientCredentials(
              accessKey: 'xxxxxxxxxxx',
              secretKey: 'xxxxxxxxxxx'));
      final bucket = 'xxxxx';
      final file = File(filePath);
      Uint8List fileBytes = await streamToUint8List(file.openRead());

      s3.putObject(
          bucket: bucket,
          key: mediaURL,
          body: fileBytes,
          contentLength: fileBytes.length);
    } catch (err) {
      succeeded = false;
      desc = err.toString();
    } finally {
      await dioClient.dio.post('/api/上传完成', data: {
        'assetID': assetId,
        'status': succeeded ? 100 : 50,
        'desc': desc,
      });
    }
  }
转载自:https://juejin.cn/post/7366874332955770890
评论
请登录