From 9efff6aeb485f8e6c7536413c72215e1a39a3f7c Mon Sep 17 00:00:00 2001 From: lcy <2503978335@qq.com> Date: Fri, 7 Jul 2023 18:12:06 +0800 Subject: [PATCH] feat:视频跟读逻辑修改 --- ios/Runner/XSMessageMehtodChannel.swift | 2 +- lib/pages/repeatafter/widgets/repeat_after_item.dart | 11 +++++------ lib/pages/repeataftercontent/bloc/repeat_after_content_bloc.dart | 272 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------------------------------------------------------- lib/pages/repeataftercontent/bloc/repeat_after_content_event.dart | 17 +++++++++++------ lib/pages/repeataftercontent/repeat_after_content_page.dart | 42 +++++++++++++++++++++++++++++------------- pubspec.yaml | 2 ++ 6 files changed, 245 insertions(+), 101 deletions(-) diff --git a/ios/Runner/XSMessageMehtodChannel.swift b/ios/Runner/XSMessageMehtodChannel.swift index 2b0f76f..4243db0 100644 --- a/ios/Runner/XSMessageMehtodChannel.swift +++ b/ios/Runner/XSMessageMehtodChannel.swift @@ -78,7 +78,7 @@ class XSMessageMehtodChannel: NSObject,SSOralEvaluatingManagerDelegate { } else { config.oralType = .sentence } - config.oralType = .kidSent + config.oralType = .sentence config.userId = userId SSOralEvaluatingManager.share().startEvaluateOral(withWavPath: voicePath, config: config) } diff --git a/lib/pages/repeatafter/widgets/repeat_after_item.dart b/lib/pages/repeatafter/widgets/repeat_after_item.dart index 26c99f6..b796929 100644 --- a/lib/pages/repeatafter/widgets/repeat_after_item.dart +++ b/lib/pages/repeatafter/widgets/repeat_after_item.dart @@ -20,12 +20,11 @@ class RepeatAfterItem extends StatelessWidget { child: GestureDetector( onTap: (){ ///todo 暂时注释调,测试用 - // if (entity != null) { - // if (!entity!.lock!) { - // tapEvent?.call(); - // } - // } - tapEvent?.call(); + if (entity != null) { + if (!entity!.lock!) { + tapEvent?.call(); + } + } }, child: Stack( children: [ diff --git a/lib/pages/repeataftercontent/bloc/repeat_after_content_bloc.dart b/lib/pages/repeataftercontent/bloc/repeat_after_content_bloc.dart index d47eb23..14ebf5a 100644 --- a/lib/pages/repeataftercontent/bloc/repeat_after_content_bloc.dart +++ b/lib/pages/repeataftercontent/bloc/repeat_after_content_bloc.dart @@ -1,7 +1,13 @@ -import 'package:audioplayers/audioplayers.dart'; +import 'dart:io'; +import 'dart:async'; + +import 'package:audio_session/audio_session.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_sound/flutter_sound.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:wow_english/common/request/dao/listen_dao.dart'; import '../../../common/request/exception.dart'; import '../../../models/read_content_entity.dart'; @@ -23,47 +29,56 @@ enum VoiceRecordState { voiceRecordEnd } -enum VoicePlayState { +///先声测评状态 +enum XSVoiceCheckState { ///未知 unKnow, - ///播放中 - playing, - ///播放完成 - completed, - ///播放终止 - stop + ///测评开始 + start, + ///测评结束 + stop, } class RepeatAfterContentBloc extends Bloc { final String courseLessonId; - ///是否正在播放视频 + /// 是否正在播放视频 bool _videoPlaying = true; - ///是否需要录音 + bool get videoPlaying => _videoPlaying; + /// 是否正在录音 bool _isRecord = false; - + bool get isRecord => _isRecord; + /// 先声评测状态 + XSVoiceCheckState _xSCheckState = XSVoiceCheckState.unKnow; + XSVoiceCheckState get xSCheckState => _xSCheckState; + /// 评测结果 Map? _voiceTestResult; - + Map? get voiceTestResult => _voiceTestResult; + /// 录音的次数 int _recordNumber = 0; - + /// 录音文件地址 + String _path = ''; + String get path => _path; + /// 当前播放的视频位置 + int _currentPlayIndex = 0; + int get currentPlayIndex => _currentPlayIndex; + + /// 录音状态 VoiceRecordState _voiceRecordState = VoiceRecordState.voiceRecordUnkonw; - - bool get videoPlaying => _videoPlaying; - - bool get isRecord => _isRecord; - VoiceRecordState get voiceRecordState => _voiceRecordState; + /// 跟读内容数字 List? _entityList; - List? get entityList => _entityList ; - Map? get voiceTestResult => _voiceTestResult; - + /// 方法 late MethodChannel methodChannel; - late AudioPlayer audioPlayer; + ///录音 + late FlutterSoundRecorder _soundRecorder; + late FlutterSoundPlayer _soundPlayer; + StreamSubscription? _soundPlayerListen; RepeatAfterContentBloc(this.courseLessonId) : super(RepeatAfterContentInitial()) { on(_voiceRecordStateChange); @@ -71,60 +86,67 @@ class RepeatAfterContentBloc extends Bloc(_changeVideoPlayIndex); on(_videoPlayStateChange); on(_recordeVoicePlay); + on(_starRecordVoice); + on(_stopRecordVoice); on(_voiceXsResult); on(_initVoiceSdk); on(_requestData); on(_voiceXsTest); on(_voiceXsStop); on(_voiceRecord); - on((event, emit) { - //音频播放器 - audioPlayer = AudioPlayer(); - audioPlayer.onPlayerStateChanged.listen((event) async { - debugPrint('播放状态变化'); - if (event == PlayerState.completed) { - debugPrint('播放完成'); - - } - if (event == PlayerState.stopped) { - debugPrint('播放结束'); - - } - - if (event == PlayerState.playing) { - debugPrint('正在播放中'); - - } - if(isClosed) { - return; - } - - }); + on(_initBlocData); + } - methodChannel = const MethodChannel('wow_english/sing_sound_method_channel'); - methodChannel.setMethodCallHandler((call) async { - if (call.method == 'voiceResult') {//评测结果 - add(XSVoiceResultEvent(call.arguments)); - add(PostFollowReadContentEvent()); - return; - } + @override + Future close() { + _releaseFlauto(); + return super.close(); + } - if (call.method == 'voiceStart') {//评测开始 - debugPrint('评测开始'); - return; - } + ///初始化功能 + void _initBlocData(InitBlocEvent event, Emitter emitter) { + methodChannel = const MethodChannel('wow_english/sing_sound_method_channel'); + methodChannel.setMethodCallHandler((call) async { + if (call.method == 'voiceResult') {//评测结果 + add(XSVoiceResultEvent(call.arguments)); + add(PostFollowReadContentEvent()); + return; + } + }); - if (call.method == 'voiceEnd') {//评测结束 - debugPrint('评测结束'); - return; - } + //录音 + _soundRecorder = FlutterSoundRecorder(); + _init(); + } - if (call.method == 'voiceFail') {//评测失败 - showToast('评测失败'); - return; - } - }); - }); + void _init() async { + await _soundRecorder.openRecorder(); + await _soundRecorder.setSubscriptionDuration(const Duration(milliseconds: 10)); + + //音屏 + _soundPlayer = FlutterSoundPlayer(); + //设置音频 + 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, + )); + await _soundPlayer.closePlayer(); + await _soundPlayer.openPlayer(); + await _soundPlayer.setSubscriptionDuration(const Duration(milliseconds: 10)); } ///请求数据 @@ -177,12 +199,17 @@ class RepeatAfterContentBloc extends Bloc emitter) async { - await audioPlayer.stop(); methodChannel.invokeMethod( - 'startVoice', - {'word':event.testWord,'type':event.type,'userId':event.userId.toString()} + 'starLocalVoice', + { + 'type':event.type, + 'word':event.testWord, + 'voicePath':_path, + 'userId':event.userId.toString() + } ); _recordNumber++; + _xSCheckState = XSVoiceCheckState.start; emitter(XSVoiceTestState()); } @@ -196,21 +223,116 @@ class RepeatAfterContentBloc extends Bloc emitter) async { - await audioPlayer.stop(); - assert(event.audioUrl.isNotEmpty); - await audioPlayer.play(UrlSource(event.audioUrl)); - } + if (await _fileExists(_path)) { + if (_soundPlayer.isPlaying) { + _soundPlayer.stopPlayer(); + } + await _soundPlayer.startPlayer( + fromURI: path, + codec: Codec.aacADTS, + sampleRate: 44000, + whenFinished: (){ + + } + ); + } + } ///更改播放的视频 void _changeVideoPlayIndex(ChangeVideoPlayIndexEvent event,Emitter emitter) async { + if (_entityList == null || _entityList!.isEmpty) { + return; + } + if (event.isNext) { + if (_currentPlayIndex < _entityList!.length-1) { + _currentPlayIndex++; + } + } else { + if (_currentPlayIndex >0) { + _currentPlayIndex--; + } + } emitter(ChangeVideoPlayIndexState(event.isNext)); } + + ///开始录音 + void _starRecordVoice(StarRecordVoiceEvent event,Emitter emitter) async { + try { + await getPermissionStatus().then((value) async { + if (!value) { + debugPrint('失败$value'); + return; + } + Directory tempDir = await getTemporaryDirectory(); + var time = DateTime.now().millisecondsSinceEpoch; + String path = '${tempDir.path}/$time${ext[Codec.aacADTS.index]}'; + + _path = path; + debugPrint('=====> 准备开始录音'); + await _soundRecorder.startRecorder( + toFile: path, + codec: Codec.aacADTS, + bitRate: 8000, + numChannels: 1, + sampleRate: 8000, + ); + debugPrint('=====> 开始录音'); + _voiceRecordState = VoiceRecordState.voiceRecording; + emitter(VoiceRecordStateChange()); + }); + } catch (error) { + await _soundRecorder.stopRecorder(); + } + } + + ///停止录音 + void _stopRecordVoice(StopRecordVoiceEvent event,Emitter emitter) async { + debugPrint('=====> 停止录音'); + await _soundRecorder.stopRecorder(); + _voiceRecordState = VoiceRecordState.voiceRecordEnd; + emitter(VoiceRecordStateChange()); + } + + /// 判断文件是否存在 + Future _fileExists(String path) async { + return await File(path).exists(); + } + + ///获取权限 + Future getPermissionStatus() async { + Permission permission = Permission.microphone; + PermissionStatus status = await permission.status; + if (status.isGranted) { + return true; + } else if (status.isDenied) { + requestPermission(permission); + } else if (status.isPermanentlyDenied) { + openAppSettings(); + } else if (status.isRestricted) { + requestPermission(permission); + } else { + + } + return false; + } + + /// 释放录音 + Future _releaseFlauto() async { + await _soundRecorder.closeRecorder(); + } + + ///申请权限 + void requestPermission(Permission permission) async { + PermissionStatus status = await permission.request(); + if (status.isPermanentlyDenied) { + openAppSettings(); + } + } } diff --git a/lib/pages/repeataftercontent/bloc/repeat_after_content_event.dart b/lib/pages/repeataftercontent/bloc/repeat_after_content_event.dart index 9b48e16..5a3797e 100644 --- a/lib/pages/repeataftercontent/bloc/repeat_after_content_event.dart +++ b/lib/pages/repeataftercontent/bloc/repeat_after_content_event.dart @@ -6,9 +6,9 @@ abstract class RepeatAfterContentEvent {} class InitBlocEvent extends RepeatAfterContentEvent {} class VideoPlayChangeEvent extends RepeatAfterContentEvent {} - +///切换录音状态 class VoiceRecordEvent extends RepeatAfterContentEvent {} - +///请求数据 class RequestDataEvent extends RepeatAfterContentEvent {} class VoiceRecordStateChangeEvent extends RepeatAfterContentEvent { @@ -39,13 +39,18 @@ class XSVoiceResultEvent extends RepeatAfterContentEvent { XSVoiceResultEvent(this.message); } -class RecordeVoicePlayEvent extends RepeatAfterContentEvent { - final String audioUrl; - RecordeVoicePlayEvent(this.audioUrl); -} +///开始录音 +class StarRecordVoiceEvent extends RepeatAfterContentEvent {} + +///停止录音 +class StopRecordVoiceEvent extends RepeatAfterContentEvent {} + +///播放录音 +class RecordeVoicePlayEvent extends RepeatAfterContentEvent {} class PostFollowReadContentEvent extends RepeatAfterContentEvent {} +///切换视频播放 class ChangeVideoPlayIndexEvent extends RepeatAfterContentEvent { final bool isNext; ChangeVideoPlayIndexEvent(this.isNext); diff --git a/lib/pages/repeataftercontent/repeat_after_content_page.dart b/lib/pages/repeataftercontent/repeat_after_content_page.dart index ec24037..fc783f0 100644 --- a/lib/pages/repeataftercontent/repeat_after_content_page.dart +++ b/lib/pages/repeataftercontent/repeat_after_content_page.dart @@ -7,6 +7,7 @@ import 'package:wow_english/route/route.dart'; import '../../common/core/app_consts.dart'; import '../../common/core/user_util.dart'; +import '../../models/read_content_entity.dart'; import '../../utils/toast_util.dart'; import 'widgets/repeat_video_widget.dart'; @@ -39,7 +40,14 @@ class _RepeatAfterContentPage extends StatelessWidget { Widget build(BuildContext context) { return BlocListener( listener: (context,state){ - + final bloc = BlocProvider.of(context); + if (state is VoiceRecordStateChange) {//录音状态回调 + if (bloc.voiceRecordState == VoiceRecordState.voiceRecordEnd) {//声音录制结束 + ReadContentEntity? readContentEntity = bloc.entityList?[bloc.currentPlayIndex]; + bloc.add(XSVoiceTestEvent(readContentEntity?.word??'','0',UserUtil.getUser()!.id.toString())); + } + return; + } }, child: _repeatAfterContentView(), ); @@ -248,7 +256,7 @@ class _RepeatAfterContentPage extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.end, children: [ Offstage( - offstage: bloc.voiceRecordState != VoiceRecordState.voiceRecordEnd && voiceResult == null, + offstage:!(bloc.voiceRecordState == VoiceRecordState.voiceRecordEnd && bloc.xSCheckState == XSVoiceCheckState.stop), child: Column( children: [ Container( @@ -270,9 +278,7 @@ class _RepeatAfterContentPage extends StatelessWidget { ), IconButton( onPressed: (){ - if(voiceResult != null) { - bloc.add(RecordeVoicePlayEvent(voiceResult['audioUrl']??'')); - } + bloc.add(RecordeVoicePlayEvent()); }, icon: Image.asset( 'voice_record_play'.assetPng, @@ -291,19 +297,29 @@ class _RepeatAfterContentPage extends StatelessWidget { ], ), ), + Offstage( + offstage: bloc.voiceRecordState == VoiceRecordState.voiceRecordUnkonw || bloc.xSCheckState != XSVoiceCheckState.unKnow, + child: Container( + color: Colors.grey, + padding: EdgeInsets.symmetric( + vertical: 50.h, + horizontal: 50.w + ), + child: Text( + bloc.voiceRecordState == VoiceRecordState.voiceRecording?'正在录音':'录音结束' + ), + ), + ), + 10.verticalSpace, GestureDetector( onTap: () => bloc.add(VoiceRecordEvent()), - onLongPress: () { - bloc.add(XSVoiceTestEvent(bloc.entityList?.first?.word??'', '0', UserUtil.getUser()!.id.toString())); - }, onLongPressStart: (LongPressStartDetails details) { - bloc.add(VoiceRecordStateChangeEvent(VoiceRecordState.voiceRecordStat)); + ///开始录音 + bloc.add(StarRecordVoiceEvent()); }, onLongPressEnd: (LongPressEndDetails details) { - bloc.add(VoiceRecordStateChangeEvent(VoiceRecordState.voiceRecordEnd)); - }, - onLongPressUp: () { - + ///结束录音 + bloc.add(StopRecordVoiceEvent()); }, child: Image.asset( 'video_record'.assetPng, diff --git a/pubspec.yaml b/pubspec.yaml index dc98d45..f3fad3e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -95,6 +95,8 @@ dependencies: audioplayers: ^4.1.0 # 语音录制 https://pub.dev/packages/flutter_sound flutter_sound: ^9.2.13 + # 音频播放 https://pub.dev/packages/audio_session + audio_session: ^0.1.16 # 文件管理 https://pub.dev/packages/path_provider path_provider: ^2.0.15 # 阿里云oss https://pub.dev/packages/flutter_oss_aliyun -- libgit2 0.22.2