Flutter-Android-APP实现音频进行提取并上传s3服务器
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
那么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
-
转码视频文件:
'-i input.mp4 -c:v libx264 -c:a aac output.mp4'
-
裁剪视频:
'-i input.mp4 -ss 00:00:10 -t 00:00:05 -c:v copy -c:a copy output.mp4'
-
提取音频:
'-i input.mp4 -vn -acodec copy output.m4a'
-
合并音视频:
'-i input.mp4 -i input.mp3 -c:v copy -c:a aac -strict experimental output.mp4'
-
改变分辨率:
'-i input.mp4 -vf scale=640:360 output.mp4'
-
改变帧率:
'-i input.mp4 -r 24 output.mp4'
-
添加水印:
'-i input.mp4 -i watermark.png -filter_complex "overlay=10:10" output.mp4'
-
提取关键帧:
'-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');
}
}
抽取的音频存储在了虚拟机的缓存目录中
使用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