Commit 9efff6aeb485f8e6c7536413c72215e1a39a3f7c
1 parent
b830911b
feat:视频跟读逻辑修改
Showing
6 changed files
with
245 additions
and
101 deletions
ios/Runner/XSMessageMehtodChannel.swift
... | ... | @@ -78,7 +78,7 @@ class XSMessageMehtodChannel: NSObject,SSOralEvaluatingManagerDelegate { |
78 | 78 | } else { |
79 | 79 | config.oralType = .sentence |
80 | 80 | } |
81 | - config.oralType = .kidSent | |
81 | + config.oralType = .sentence | |
82 | 82 | config.userId = userId |
83 | 83 | SSOralEvaluatingManager.share().startEvaluateOral(withWavPath: voicePath, config: config) |
84 | 84 | } | ... | ... |
lib/pages/repeatafter/widgets/repeat_after_item.dart
... | ... | @@ -20,12 +20,11 @@ class RepeatAfterItem extends StatelessWidget { |
20 | 20 | child: GestureDetector( |
21 | 21 | onTap: (){ |
22 | 22 | ///todo 暂时注释调,测试用 |
23 | - // if (entity != null) { | |
24 | - // if (!entity!.lock!) { | |
25 | - // tapEvent?.call(); | |
26 | - // } | |
27 | - // } | |
28 | - tapEvent?.call(); | |
23 | + if (entity != null) { | |
24 | + if (!entity!.lock!) { | |
25 | + tapEvent?.call(); | |
26 | + } | |
27 | + } | |
29 | 28 | }, |
30 | 29 | child: Stack( |
31 | 30 | children: [ | ... | ... |
lib/pages/repeataftercontent/bloc/repeat_after_content_bloc.dart
1 | -import 'package:audioplayers/audioplayers.dart'; | |
1 | +import 'dart:io'; | |
2 | +import 'dart:async'; | |
3 | + | |
4 | +import 'package:audio_session/audio_session.dart'; | |
2 | 5 | import 'package:flutter/cupertino.dart'; |
3 | 6 | import 'package:flutter/services.dart'; |
4 | 7 | import 'package:flutter_bloc/flutter_bloc.dart'; |
8 | +import 'package:flutter_sound/flutter_sound.dart'; | |
9 | +import 'package:path_provider/path_provider.dart'; | |
10 | +import 'package:permission_handler/permission_handler.dart'; | |
5 | 11 | import 'package:wow_english/common/request/dao/listen_dao.dart'; |
6 | 12 | import '../../../common/request/exception.dart'; |
7 | 13 | import '../../../models/read_content_entity.dart'; |
... | ... | @@ -23,47 +29,56 @@ enum VoiceRecordState { |
23 | 29 | voiceRecordEnd |
24 | 30 | } |
25 | 31 | |
26 | -enum VoicePlayState { | |
32 | +///先声测评状态 | |
33 | +enum XSVoiceCheckState { | |
27 | 34 | ///未知 |
28 | 35 | unKnow, |
29 | - ///播放中 | |
30 | - playing, | |
31 | - ///播放完成 | |
32 | - completed, | |
33 | - ///播放终止 | |
34 | - stop | |
36 | + ///测评开始 | |
37 | + start, | |
38 | + ///测评结束 | |
39 | + stop, | |
35 | 40 | } |
36 | 41 | |
37 | 42 | class RepeatAfterContentBloc extends Bloc<RepeatAfterContentEvent, RepeatAfterContentState> { |
38 | 43 | |
39 | 44 | final String courseLessonId; |
40 | 45 | |
41 | - ///是否正在播放视频 | |
46 | + /// 是否正在播放视频 | |
42 | 47 | bool _videoPlaying = true; |
43 | - ///是否需要录音 | |
48 | + bool get videoPlaying => _videoPlaying; | |
49 | + /// 是否正在录音 | |
44 | 50 | bool _isRecord = false; |
45 | - | |
51 | + bool get isRecord => _isRecord; | |
52 | + /// 先声评测状态 | |
53 | + XSVoiceCheckState _xSCheckState = XSVoiceCheckState.unKnow; | |
54 | + XSVoiceCheckState get xSCheckState => _xSCheckState; | |
55 | + /// 评测结果 | |
46 | 56 | Map? _voiceTestResult; |
47 | - | |
57 | + Map? get voiceTestResult => _voiceTestResult; | |
58 | + /// 录音的次数 | |
48 | 59 | int _recordNumber = 0; |
49 | - | |
60 | + /// 录音文件地址 | |
61 | + String _path = ''; | |
62 | + String get path => _path; | |
63 | + /// 当前播放的视频位置 | |
64 | + int _currentPlayIndex = 0; | |
65 | + int get currentPlayIndex => _currentPlayIndex; | |
66 | + | |
67 | + /// 录音状态 | |
50 | 68 | VoiceRecordState _voiceRecordState = VoiceRecordState.voiceRecordUnkonw; |
51 | - | |
52 | - bool get videoPlaying => _videoPlaying; | |
53 | - | |
54 | - bool get isRecord => _isRecord; | |
55 | - | |
56 | 69 | VoiceRecordState get voiceRecordState => _voiceRecordState; |
57 | 70 | |
71 | + /// 跟读内容数字 | |
58 | 72 | List<ReadContentEntity?>? _entityList; |
59 | - | |
60 | 73 | List<ReadContentEntity?>? get entityList => _entityList ; |
61 | 74 | |
62 | - Map? get voiceTestResult => _voiceTestResult; | |
63 | - | |
75 | + /// 方法 | |
64 | 76 | late MethodChannel methodChannel; |
65 | 77 | |
66 | - late AudioPlayer audioPlayer; | |
78 | + ///录音 | |
79 | + late FlutterSoundRecorder _soundRecorder; | |
80 | + late FlutterSoundPlayer _soundPlayer; | |
81 | + StreamSubscription? _soundPlayerListen; | |
67 | 82 | |
68 | 83 | RepeatAfterContentBloc(this.courseLessonId) : super(RepeatAfterContentInitial()) { |
69 | 84 | on<VoiceRecordStateChangeEvent>(_voiceRecordStateChange); |
... | ... | @@ -71,60 +86,67 @@ class RepeatAfterContentBloc extends Bloc<RepeatAfterContentEvent, RepeatAfterCo |
71 | 86 | on<ChangeVideoPlayIndexEvent>(_changeVideoPlayIndex); |
72 | 87 | on<VideoPlayChangeEvent>(_videoPlayStateChange); |
73 | 88 | on<RecordeVoicePlayEvent>(_recordeVoicePlay); |
89 | + on<StarRecordVoiceEvent>(_starRecordVoice); | |
90 | + on<StopRecordVoiceEvent>(_stopRecordVoice); | |
74 | 91 | on<XSVoiceResultEvent>(_voiceXsResult); |
75 | 92 | on<XSVoiceInitEvent>(_initVoiceSdk); |
76 | 93 | on<RequestDataEvent>(_requestData); |
77 | 94 | on<XSVoiceTestEvent>(_voiceXsTest); |
78 | 95 | on<XSVoiceStopEvent>(_voiceXsStop); |
79 | 96 | on<VoiceRecordEvent>(_voiceRecord); |
80 | - on<InitBlocEvent>((event, emit) { | |
81 | - //音频播放器 | |
82 | - audioPlayer = AudioPlayer(); | |
83 | - audioPlayer.onPlayerStateChanged.listen((event) async { | |
84 | - debugPrint('播放状态变化'); | |
85 | - if (event == PlayerState.completed) { | |
86 | - debugPrint('播放完成'); | |
87 | - | |
88 | - } | |
89 | - if (event == PlayerState.stopped) { | |
90 | - debugPrint('播放结束'); | |
91 | - | |
92 | - } | |
93 | - | |
94 | - if (event == PlayerState.playing) { | |
95 | - debugPrint('正在播放中'); | |
96 | - | |
97 | - } | |
98 | - if(isClosed) { | |
99 | - return; | |
100 | - } | |
101 | - | |
102 | - }); | |
97 | + on<InitBlocEvent>(_initBlocData); | |
98 | + } | |
103 | 99 | |
104 | - methodChannel = const MethodChannel('wow_english/sing_sound_method_channel'); | |
105 | - methodChannel.setMethodCallHandler((call) async { | |
106 | - if (call.method == 'voiceResult') {//评测结果 | |
107 | - add(XSVoiceResultEvent(call.arguments)); | |
108 | - add(PostFollowReadContentEvent()); | |
109 | - return; | |
110 | - } | |
100 | + @override | |
101 | + Future<void> close() { | |
102 | + _releaseFlauto(); | |
103 | + return super.close(); | |
104 | + } | |
111 | 105 | |
112 | - if (call.method == 'voiceStart') {//评测开始 | |
113 | - debugPrint('评测开始'); | |
114 | - return; | |
115 | - } | |
106 | + ///初始化功能 | |
107 | + void _initBlocData(InitBlocEvent event, Emitter<RepeatAfterContentState> emitter) { | |
108 | + methodChannel = const MethodChannel('wow_english/sing_sound_method_channel'); | |
109 | + methodChannel.setMethodCallHandler((call) async { | |
110 | + if (call.method == 'voiceResult') {//评测结果 | |
111 | + add(XSVoiceResultEvent(call.arguments)); | |
112 | + add(PostFollowReadContentEvent()); | |
113 | + return; | |
114 | + } | |
115 | + }); | |
116 | 116 | |
117 | - if (call.method == 'voiceEnd') {//评测结束 | |
118 | - debugPrint('评测结束'); | |
119 | - return; | |
120 | - } | |
117 | + //录音 | |
118 | + _soundRecorder = FlutterSoundRecorder(); | |
119 | + _init(); | |
120 | + } | |
121 | 121 | |
122 | - if (call.method == 'voiceFail') {//评测失败 | |
123 | - showToast('评测失败'); | |
124 | - return; | |
125 | - } | |
126 | - }); | |
127 | - }); | |
122 | + void _init() async { | |
123 | + await _soundRecorder.openRecorder(); | |
124 | + await _soundRecorder.setSubscriptionDuration(const Duration(milliseconds: 10)); | |
125 | + | |
126 | + //音屏 | |
127 | + _soundPlayer = FlutterSoundPlayer(); | |
128 | + //设置音频 | |
129 | + final session = await AudioSession.instance; | |
130 | + await session.configure(AudioSessionConfiguration( | |
131 | + avAudioSessionCategory: AVAudioSessionCategory.playAndRecord, | |
132 | + avAudioSessionCategoryOptions: | |
133 | + AVAudioSessionCategoryOptions.allowBluetooth | | |
134 | + AVAudioSessionCategoryOptions.defaultToSpeaker, | |
135 | + avAudioSessionMode: AVAudioSessionMode.spokenAudio, | |
136 | + avAudioSessionRouteSharingPolicy: | |
137 | + AVAudioSessionRouteSharingPolicy.defaultPolicy, | |
138 | + avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.none, | |
139 | + androidAudioAttributes: const AndroidAudioAttributes( | |
140 | + contentType: AndroidAudioContentType.speech, | |
141 | + flags: AndroidAudioFlags.none, | |
142 | + usage: AndroidAudioUsage.voiceCommunication, | |
143 | + ), | |
144 | + androidAudioFocusGainType: AndroidAudioFocusGainType.gain, | |
145 | + androidWillPauseWhenDucked: true, | |
146 | + )); | |
147 | + await _soundPlayer.closePlayer(); | |
148 | + await _soundPlayer.openPlayer(); | |
149 | + await _soundPlayer.setSubscriptionDuration(const Duration(milliseconds: 10)); | |
128 | 150 | } |
129 | 151 | |
130 | 152 | ///请求数据 |
... | ... | @@ -177,12 +199,17 @@ class RepeatAfterContentBloc extends Bloc<RepeatAfterContentEvent, RepeatAfterCo |
177 | 199 | |
178 | 200 | ///先声测试 |
179 | 201 | void _voiceXsTest(XSVoiceTestEvent event,Emitter<RepeatAfterContentState> emitter) async { |
180 | - await audioPlayer.stop(); | |
181 | 202 | methodChannel.invokeMethod( |
182 | - 'startVoice', | |
183 | - {'word':event.testWord,'type':event.type,'userId':event.userId.toString()} | |
203 | + 'starLocalVoice', | |
204 | + { | |
205 | + 'type':event.type, | |
206 | + 'word':event.testWord, | |
207 | + 'voicePath':_path, | |
208 | + 'userId':event.userId.toString() | |
209 | + } | |
184 | 210 | ); |
185 | 211 | _recordNumber++; |
212 | + _xSCheckState = XSVoiceCheckState.start; | |
186 | 213 | emitter(XSVoiceTestState()); |
187 | 214 | } |
188 | 215 | |
... | ... | @@ -196,21 +223,116 @@ class RepeatAfterContentBloc extends Bloc<RepeatAfterContentEvent, RepeatAfterCo |
196 | 223 | final Map args = event.message as Map; |
197 | 224 | final result = args['result'] as Map; |
198 | 225 | final overall = result['overall'].toString(); |
199 | - final audioUrl = args['audioUrl'].toString(); | |
200 | - _voiceTestResult = {'overall':overall,'audioUrl':audioUrl}; | |
226 | + _voiceTestResult = {'overall':overall}; | |
201 | 227 | emitter(XSVoiceTestState()); |
202 | 228 | } |
203 | 229 | |
204 | 230 | ///播放声音 |
205 | 231 | void _recordeVoicePlay(RecordeVoicePlayEvent event,Emitter<RepeatAfterContentState> emitter) async { |
206 | - await audioPlayer.stop(); | |
207 | - assert(event.audioUrl.isNotEmpty); | |
208 | - await audioPlayer.play(UrlSource(event.audioUrl)); | |
209 | - } | |
232 | + if (await _fileExists(_path)) { | |
233 | + if (_soundPlayer.isPlaying) { | |
234 | + _soundPlayer.stopPlayer(); | |
235 | + } | |
210 | 236 | |
237 | + await _soundPlayer.startPlayer( | |
238 | + fromURI: path, | |
239 | + codec: Codec.aacADTS, | |
240 | + sampleRate: 44000, | |
241 | + whenFinished: (){ | |
242 | + | |
243 | + } | |
244 | + ); | |
245 | + } | |
246 | + } | |
211 | 247 | |
212 | 248 | ///更改播放的视频 |
213 | 249 | void _changeVideoPlayIndex(ChangeVideoPlayIndexEvent event,Emitter<RepeatAfterContentState> emitter) async { |
250 | + if (_entityList == null || _entityList!.isEmpty) { | |
251 | + return; | |
252 | + } | |
253 | + if (event.isNext) { | |
254 | + if (_currentPlayIndex < _entityList!.length-1) { | |
255 | + _currentPlayIndex++; | |
256 | + } | |
257 | + } else { | |
258 | + if (_currentPlayIndex >0) { | |
259 | + _currentPlayIndex--; | |
260 | + } | |
261 | + } | |
214 | 262 | emitter(ChangeVideoPlayIndexState(event.isNext)); |
215 | 263 | } |
264 | + | |
265 | + ///开始录音 | |
266 | + void _starRecordVoice(StarRecordVoiceEvent event,Emitter<RepeatAfterContentState> emitter) async { | |
267 | + try { | |
268 | + await getPermissionStatus().then((value) async { | |
269 | + if (!value) { | |
270 | + debugPrint('失败$value'); | |
271 | + return; | |
272 | + } | |
273 | + Directory tempDir = await getTemporaryDirectory(); | |
274 | + var time = DateTime.now().millisecondsSinceEpoch; | |
275 | + String path = '${tempDir.path}/$time${ext[Codec.aacADTS.index]}'; | |
276 | + | |
277 | + _path = path; | |
278 | + debugPrint('=====> 准备开始录音'); | |
279 | + await _soundRecorder.startRecorder( | |
280 | + toFile: path, | |
281 | + codec: Codec.aacADTS, | |
282 | + bitRate: 8000, | |
283 | + numChannels: 1, | |
284 | + sampleRate: 8000, | |
285 | + ); | |
286 | + debugPrint('=====> 开始录音'); | |
287 | + _voiceRecordState = VoiceRecordState.voiceRecording; | |
288 | + emitter(VoiceRecordStateChange()); | |
289 | + }); | |
290 | + } catch (error) { | |
291 | + await _soundRecorder.stopRecorder(); | |
292 | + } | |
293 | + } | |
294 | + | |
295 | + ///停止录音 | |
296 | + void _stopRecordVoice(StopRecordVoiceEvent event,Emitter<RepeatAfterContentState> emitter) async { | |
297 | + debugPrint('=====> 停止录音'); | |
298 | + await _soundRecorder.stopRecorder(); | |
299 | + _voiceRecordState = VoiceRecordState.voiceRecordEnd; | |
300 | + emitter(VoiceRecordStateChange()); | |
301 | + } | |
302 | + | |
303 | + /// 判断文件是否存在 | |
304 | + Future<bool> _fileExists(String path) async { | |
305 | + return await File(path).exists(); | |
306 | + } | |
307 | + | |
308 | + ///获取权限 | |
309 | + Future<bool> getPermissionStatus() async { | |
310 | + Permission permission = Permission.microphone; | |
311 | + PermissionStatus status = await permission.status; | |
312 | + if (status.isGranted) { | |
313 | + return true; | |
314 | + } else if (status.isDenied) { | |
315 | + requestPermission(permission); | |
316 | + } else if (status.isPermanentlyDenied) { | |
317 | + openAppSettings(); | |
318 | + } else if (status.isRestricted) { | |
319 | + requestPermission(permission); | |
320 | + } else { | |
321 | + | |
322 | + } | |
323 | + return false; | |
324 | + } | |
325 | + | |
326 | + /// 释放录音 | |
327 | + Future<void> _releaseFlauto() async { | |
328 | + await _soundRecorder.closeRecorder(); | |
329 | + } | |
330 | + | |
331 | + ///申请权限 | |
332 | + void requestPermission(Permission permission) async { | |
333 | + PermissionStatus status = await permission.request(); | |
334 | + if (status.isPermanentlyDenied) { | |
335 | + openAppSettings(); | |
336 | + } | |
337 | + } | |
216 | 338 | } | ... | ... |
lib/pages/repeataftercontent/bloc/repeat_after_content_event.dart
... | ... | @@ -6,9 +6,9 @@ abstract class RepeatAfterContentEvent {} |
6 | 6 | class InitBlocEvent extends RepeatAfterContentEvent {} |
7 | 7 | |
8 | 8 | class VideoPlayChangeEvent extends RepeatAfterContentEvent {} |
9 | - | |
9 | +///切换录音状态 | |
10 | 10 | class VoiceRecordEvent extends RepeatAfterContentEvent {} |
11 | - | |
11 | +///请求数据 | |
12 | 12 | class RequestDataEvent extends RepeatAfterContentEvent {} |
13 | 13 | |
14 | 14 | class VoiceRecordStateChangeEvent extends RepeatAfterContentEvent { |
... | ... | @@ -39,13 +39,18 @@ class XSVoiceResultEvent extends RepeatAfterContentEvent { |
39 | 39 | XSVoiceResultEvent(this.message); |
40 | 40 | } |
41 | 41 | |
42 | -class RecordeVoicePlayEvent extends RepeatAfterContentEvent { | |
43 | - final String audioUrl; | |
44 | - RecordeVoicePlayEvent(this.audioUrl); | |
45 | -} | |
42 | +///开始录音 | |
43 | +class StarRecordVoiceEvent extends RepeatAfterContentEvent {} | |
44 | + | |
45 | +///停止录音 | |
46 | +class StopRecordVoiceEvent extends RepeatAfterContentEvent {} | |
47 | + | |
48 | +///播放录音 | |
49 | +class RecordeVoicePlayEvent extends RepeatAfterContentEvent {} | |
46 | 50 | |
47 | 51 | class PostFollowReadContentEvent extends RepeatAfterContentEvent {} |
48 | 52 | |
53 | +///切换视频播放 | |
49 | 54 | class ChangeVideoPlayIndexEvent extends RepeatAfterContentEvent { |
50 | 55 | final bool isNext; |
51 | 56 | ChangeVideoPlayIndexEvent(this.isNext); | ... | ... |
lib/pages/repeataftercontent/repeat_after_content_page.dart
... | ... | @@ -7,6 +7,7 @@ import 'package:wow_english/route/route.dart'; |
7 | 7 | |
8 | 8 | import '../../common/core/app_consts.dart'; |
9 | 9 | import '../../common/core/user_util.dart'; |
10 | +import '../../models/read_content_entity.dart'; | |
10 | 11 | import '../../utils/toast_util.dart'; |
11 | 12 | import 'widgets/repeat_video_widget.dart'; |
12 | 13 | |
... | ... | @@ -39,7 +40,14 @@ class _RepeatAfterContentPage extends StatelessWidget { |
39 | 40 | Widget build(BuildContext context) { |
40 | 41 | return BlocListener<RepeatAfterContentBloc,RepeatAfterContentState>( |
41 | 42 | listener: (context,state){ |
42 | - | |
43 | + final bloc = BlocProvider.of<RepeatAfterContentBloc>(context); | |
44 | + if (state is VoiceRecordStateChange) {//录音状态回调 | |
45 | + if (bloc.voiceRecordState == VoiceRecordState.voiceRecordEnd) {//声音录制结束 | |
46 | + ReadContentEntity? readContentEntity = bloc.entityList?[bloc.currentPlayIndex]; | |
47 | + bloc.add(XSVoiceTestEvent(readContentEntity?.word??'','0',UserUtil.getUser()!.id.toString())); | |
48 | + } | |
49 | + return; | |
50 | + } | |
43 | 51 | }, |
44 | 52 | child: _repeatAfterContentView(), |
45 | 53 | ); |
... | ... | @@ -248,7 +256,7 @@ class _RepeatAfterContentPage extends StatelessWidget { |
248 | 256 | mainAxisAlignment: MainAxisAlignment.end, |
249 | 257 | children: [ |
250 | 258 | Offstage( |
251 | - offstage: bloc.voiceRecordState != VoiceRecordState.voiceRecordEnd && voiceResult == null, | |
259 | + offstage:!(bloc.voiceRecordState == VoiceRecordState.voiceRecordEnd && bloc.xSCheckState == XSVoiceCheckState.stop), | |
252 | 260 | child: Column( |
253 | 261 | children: [ |
254 | 262 | Container( |
... | ... | @@ -270,9 +278,7 @@ class _RepeatAfterContentPage extends StatelessWidget { |
270 | 278 | ), |
271 | 279 | IconButton( |
272 | 280 | onPressed: (){ |
273 | - if(voiceResult != null) { | |
274 | - bloc.add(RecordeVoicePlayEvent(voiceResult['audioUrl']??'')); | |
275 | - } | |
281 | + bloc.add(RecordeVoicePlayEvent()); | |
276 | 282 | }, |
277 | 283 | icon: Image.asset( |
278 | 284 | 'voice_record_play'.assetPng, |
... | ... | @@ -291,19 +297,29 @@ class _RepeatAfterContentPage extends StatelessWidget { |
291 | 297 | ], |
292 | 298 | ), |
293 | 299 | ), |
300 | + Offstage( | |
301 | + offstage: bloc.voiceRecordState == VoiceRecordState.voiceRecordUnkonw || bloc.xSCheckState != XSVoiceCheckState.unKnow, | |
302 | + child: Container( | |
303 | + color: Colors.grey, | |
304 | + padding: EdgeInsets.symmetric( | |
305 | + vertical: 50.h, | |
306 | + horizontal: 50.w | |
307 | + ), | |
308 | + child: Text( | |
309 | + bloc.voiceRecordState == VoiceRecordState.voiceRecording?'正在录音':'录音结束' | |
310 | + ), | |
311 | + ), | |
312 | + ), | |
313 | + 10.verticalSpace, | |
294 | 314 | GestureDetector( |
295 | 315 | onTap: () => bloc.add(VoiceRecordEvent()), |
296 | - onLongPress: () { | |
297 | - bloc.add(XSVoiceTestEvent(bloc.entityList?.first?.word??'', '0', UserUtil.getUser()!.id.toString())); | |
298 | - }, | |
299 | 316 | onLongPressStart: (LongPressStartDetails details) { |
300 | - bloc.add(VoiceRecordStateChangeEvent(VoiceRecordState.voiceRecordStat)); | |
317 | + ///开始录音 | |
318 | + bloc.add(StarRecordVoiceEvent()); | |
301 | 319 | }, |
302 | 320 | onLongPressEnd: (LongPressEndDetails details) { |
303 | - bloc.add(VoiceRecordStateChangeEvent(VoiceRecordState.voiceRecordEnd)); | |
304 | - }, | |
305 | - onLongPressUp: () { | |
306 | - | |
321 | + ///结束录音 | |
322 | + bloc.add(StopRecordVoiceEvent()); | |
307 | 323 | }, |
308 | 324 | child: Image.asset( |
309 | 325 | 'video_record'.assetPng, | ... | ... |
pubspec.yaml
... | ... | @@ -95,6 +95,8 @@ dependencies: |
95 | 95 | audioplayers: ^4.1.0 |
96 | 96 | # 语音录制 https://pub.dev/packages/flutter_sound |
97 | 97 | flutter_sound: ^9.2.13 |
98 | + # 音频播放 https://pub.dev/packages/audio_session | |
99 | + audio_session: ^0.1.16 | |
98 | 100 | # 文件管理 https://pub.dev/packages/path_provider |
99 | 101 | path_provider: ^2.0.15 |
100 | 102 | # 阿里云oss https://pub.dev/packages/flutter_oss_aliyun | ... | ... |