目录
前言:
注意事项:
用到的部分组件依赖及版本:
遇到的坑
遇到的坑1:
遇到的坑2:
遇到的坑3:
遇到的坑4:
Fluuter语音录制及播放组件生命周期
Flutter录音组件生命周期图:
Flutter语音播放组件生命周期图:
代码
简单视频演示:
前言:
有好多todo没实现,这里总结一下这两天遇到的坑及简单的聊天界面布局和语音录制和播放功能,这里只实现了ios端的语音录制播放功能,android端没有测试。
注意事项:
ios端需要开启访问麦克风权限,位置在ios->Runner->Info.plist
NSMicrophoneUsageDescription
访问麦克风
用到的部分组件依赖及版本:
#语音录制、播放插件
flutter_sound: ^9.2.13
#检查权限
permission_handler: ^6.0.1
#此插件会告知操作系统您的音频应用程序的性质(例如游戏、媒体播放器、助手等)以及您的应用程序将如何处理和启动音频中断(例如电话中断)
audio_session: ^0.1.10
#uuid
uuid: ^3.0.6
遇到的坑
遇到的坑1
聊天消息布局不满一页在上方显示,满一页则停留在最底部:
解决方法
使用listview反转设置可以一直保持消息在底部,但是消息数据必须要倒序;
使用Container的向上居中可以使子元素撑不满一屏时向上显示。
遇到的坑2
在iPhoneX及所有刘海屏Bottom留白问题:
解决方法
使用SafeArea安全组件可解决此问题
遇到的坑3
IOS端在Xcode Build时报错:
Undefined symbols for architecture arm64:
"___gxx_personality_v0", referenced from:
+[FlutterSound registerWithRegistrar:] in flutter_sound(FlutterSound.o)
解决方法
在Xcode Build Setting中的Other Linker Flags添加-lc++即可
遇到的坑4
点击录音按钮不提示申请权限直接报错:
排查了好久原来是检查权限工具版本的bug,改为6.0.1可成功弹出权限申请
Fluuter语音录制及播放组件生命周期
Flutter录音组件生命周期图
Flutter语音播放组件生命周期图
代码
import 'dart:math';
import 'dart:ui';
import 'package:audio_session/audio_session.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:new_chat/code/message_type.dart';
import 'package:new_chat/r.dart';
import 'package:new_chat/service/screen_adapter.dart';
import 'package:new_chat/util/time_utils.dart';
import 'package:new_chat/widget/toast_widget.dart';
import 'package:flutter_sound/flutter_sound.dart';
import 'package:flutter_sound_platform_interface/flutter_sound_recorder_platform_interface.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:logger/logger.dart' show Level;
import 'package:uuid/uuid.dart';
class SingleChatPage extends StatefulWidget {
final String chatId;
const SingleChatPage({Key? key, required this.chatId}) : super(key: key);
@override
State createState() {
return _SingleChatPageState();
}
}
class _SingleChatPageState extends State {
//message data
List _messageData = [];
///语音播放及录制定义begin
//默认语音录制为关闭
bool _keyboardVoiceEnable = false;
//listview跳转控制器
final ScrollController _scrollController = ScrollController();
//消息文本控制器
final TextEditingController _textEditingController = TextEditingController();
//语音类型
final AudioSource _theSource = AudioSource.microphone;
//存储录音编码格式
Codec _codec = Codec.aacMP4;
//播放器权限
bool _voicePlayerIsInitialized = false;
//录制权限
bool _voiceRecorderIsInitialized = false;
//播放器是否可播放
bool _voicePlayerIsReady = false;
//播放器是否在播放
bool _voicePlayerIsPlay = false;
//语音播放工具
final FlutterSoundPlayer _voicePlayer =
FlutterSoundPlayer(logLevel: Level.error);
//语音录制工具
final FlutterSoundRecorder _voiceRecorder =
FlutterSoundRecorder(logLevel: Level.error);
//存储文件后缀
String _voiceFilePathSuffix = 'temp_file.mp4';
//录音文件存储前缀
String _voiceFilePrefix = "";
///语音播放及录制定义end
@override
void initState() {
_initMessageData();
//初始化播放器
_voicePlayer.openPlayer().then((value) {
setState(() {
_voicePlayerIsInitialized = true;
});
});
//初始化录音
_initVoiceRecorder().then((value) {
setState(() {
_voiceRecorderIsInitialized = true;
});
});
super.initState();
}
@override
void dispose() {
//关闭语音播放
_voicePlayer.closePlayer();
//关闭语音录制
_voiceRecorder.closeRecorder();
super.dispose();
}
///录音及语音方法定义begin
///初始录音
///todo 用户禁止语音权限提示
Future _initVoiceRecorder() async {
if (!kIsWeb) {
var status = await Permission.microphone.request();
if (status != PermissionStatus.granted) {
throw RecordingPermissionException('Microphone permission not granted');
}
}
await _voiceRecorder.openRecorder();
if (!await _voiceRecorder.isEncoderSupported(_codec) && kIsWeb) {
_codec = Codec.opusWebM;
_voiceFilePathSuffix = 'tau_file.webm';
if (!await _voiceRecorder.isEncoderSupported(_codec) && kIsWeb) {
_voiceRecorderIsInitialized = true;
return;
}
}
final session = await AudioSession.instance;
await session.configure(AudioSessionConfiguration(
avAudioSessionCategory: AVAudioSessionCategory.playAndRecord,
avAudioSessionCategoryOptions:
AVAudioSessionCategoryOptions.allowBluetooth |
AVAudioSessionCategoryOptions.defaultToSpeaker,
avAudioSessionMode: AVAudioSessionMode.spokenAudio,
avAudioSessionRouteSharingPolicy:
AVAudioSessionRouteSharingPolicy.defaultPolicy,
avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.none,
androidAudioAttributes: const AndroidAudioAttributes(
contentType: AndroidAudioContentType.speech,
flags: AndroidAudioFlags.none,
usage: AndroidAudioUsage.voiceCommunication,
),
androidAudioFocusGainType: AndroidAudioFocusGainType.gain,
androidWillPauseWhenDucked: true,
));
_voiceRecorderIsInitialized = true;
}
///开始录音并返回录音文件前缀
String _beginVoice() {
if (!_voiceRecorderIsInitialized) {
ToastWidget.showToast("没有录音权限", ToastGravity.CENTER);
throw Exception("没有录音权限");
}
var uuid = const Uuid().v4();
_voiceRecorder
.startRecorder(
codec: _codec,
toFile: uuid + _voiceFilePathSuffix,
audioSource: _theSource)
.then((value) {
setState(() {
//播放按钮禁用并插入语音到消息中
_voicePlayerIsReady = false;
});
});
return uuid;
}
///停止录音 并将消息存储
void _stopVoice(String voiceFileId) async {
await _voiceRecorder.stopRecorder().then((value) {
setState(() {
//可以播放
_voicePlayerIsReady = true;
Map data = {};
data['messageId'] = voiceFileId;
//todo 差语音时长
data['message'] = "语音消息按钮...";
data['messageType'] = MessageType.voice;
data['messageTime'] = TimeUtils.getFormatDataString(
DateTime.now(), "yyyy-MM-dd HH:mm:ss");
data['isMe'] = Random.secure().nextBool();
//存储路径
data['messageVoice'] = voiceFileId + _voiceFilePathSuffix;
_messageData.insert(0, data);
});
});
}
///开始播放录音
void _beginPlayer(String messageVoiceFilePath) {
assert(_voicePlayerIsInitialized &&
_voicePlayerIsReady &&
_voiceRecorder.isStopped &&
_voicePlayer.isStopped);
_voicePlayer
.startPlayer(
fromURI: messageVoiceFilePath,
//codec: kIsWeb ? Codec.opusWebM : Codec.aacADTS,
//语音播放完后的动作->停止播放
whenFinished: () {
setState(() {
print("播放完的动作");
_voicePlayerIsPlay = false;
_voicePlayerIsReady = true;
});
})
.then((value) {
//语音正在播放的动作->正在播放
setState(() {
print("语音正在播放的动作");
_voicePlayerIsPlay = true;
_voicePlayerIsReady = false;
});
});
}
///停止播放声音
void _stopPlayer() {
_voicePlayer.stopPlayer().then((value) {
setState(() {
_voicePlayerIsReady = true;
_voicePlayerIsPlay = false;
});
});
}
///录音及语音方法定义end
///初始化聊天数据
//todo 差网络请求聊天数据 这里暂时mock
_initMessageData() async {
Dio dio = Dio();
//mock data
try {
//todo timeout 1 seconds
var response = await dio
.get(
"http://192.168.10.15:3000/mock/313/message",
)
.timeout(const Duration(seconds: 1));
setState(() {
_messageData = response.data['data'];
});
} catch (e) {
//mock test data
List
简单视频演示:
Flutter简单聊天界面布局及语音录制播放配套视频