Commit 9efff6aeb485f8e6c7536413c72215e1a39a3f7c

Authored by liangchengyou
1 parent b830911b

feat:视频跟读逻辑修改

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&lt;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&lt;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&lt;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 &#39;package:wow_english/route/route.dart&#39;;
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
... ...