From 133f8634487672b76f82e9a939c24ff597dc043c Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 27 Dec 2024 13:35:56 +0800 Subject: [PATCH] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E6=92=AD=E6=94=BE=E9=80=BB=E8=BE=91=E3=80=81=E8=B0=83=E6=95=B4?= =?UTF-8?q?proto=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/appRouters.dart | 8 +- lib/blue/io_tool/manager_event_bus.dart | 8 + .../star_chart_h264/star_chart_logic.dart | 276 ------------ .../star_chart_h264/star_chart_page.dart | 394 ------------------ .../star_chart_h264/star_chart_state.dart | 65 --- lib/main/lockMian/lockMain/lockMain_page.dart | 6 + .../startChart/command/message_command.dart | 2 +- lib/talk/startChart/constant/talk_status.dart | 18 + lib/talk/startChart/entity/scp_message.dart | 2 +- .../events/talk_status_change_event.dart | 13 - .../handle/impl/udp_heart_beat_handler.dart | 10 +- .../handle/impl/udp_talk_accept_handler.dart | 14 +- .../handle/impl/udp_talk_data_handler.dart | 39 +- .../handle/impl/udp_talk_expect_handler.dart | 9 +- .../handle/impl/udp_talk_hangup_handler.dart | 18 +- .../handle/impl/udp_talk_reject_handler.dart | 9 +- .../handle/impl/udp_talk_request_handler.dart | 17 +- .../handle/other/talk_data_repository.dart | 2 +- .../handle/scp_message_base_handle.dart | 14 +- .../proto/talk_data_h264_frame.proto | 18 + lib/talk/startChart/proto/talk_expect.pb.dart | 150 ++++++- .../startChart/proto/talk_expect.pbenum.dart | 36 +- .../startChart/proto/talk_expect.pbjson.dart | 66 ++- lib/talk/startChart/proto/talk_expect.proto | 44 +- lib/talk/startChart/start_chart_manage.dart | 28 +- .../startChart/start_chart_talk_status.dart | 125 ++---- .../views/talkView/talk_view_logic.dart | 291 +++++++++++++ .../views/talkView/talk_view_page.dart | 373 +++++++++++++++++ .../views/talkView/talk_view_state.dart | 68 +++ 29 files changed, 1139 insertions(+), 984 deletions(-) delete mode 100644 lib/main/lockDetail/monitoring/star_chart_h264/star_chart_logic.dart delete mode 100644 lib/main/lockDetail/monitoring/star_chart_h264/star_chart_page.dart delete mode 100644 lib/main/lockDetail/monitoring/star_chart_h264/star_chart_state.dart create mode 100644 lib/talk/startChart/constant/talk_status.dart delete mode 100644 lib/talk/startChart/events/talk_status_change_event.dart create mode 100644 lib/talk/startChart/proto/talk_data_h264_frame.proto create mode 100644 lib/talk/startChart/views/talkView/talk_view_logic.dart create mode 100644 lib/talk/startChart/views/talkView/talk_view_page.dart create mode 100644 lib/talk/startChart/views/talkView/talk_view_state.dart diff --git a/lib/appRouters.dart b/lib/appRouters.dart index 1c460c84..50aec2f6 100755 --- a/lib/appRouters.dart +++ b/lib/appRouters.dart @@ -36,7 +36,6 @@ import 'package:star_lock/main/lockDetail/messageWarn/msgNotification/msgNotific import 'package:star_lock/main/lockDetail/messageWarn/msgNotification/nDaysUnopened/nDaysUnopened_page.dart'; import 'package:star_lock/main/lockDetail/messageWarn/msgNotification/openDoorNotify/openDoorNotify_page.dart'; import 'package:star_lock/main/lockDetail/messageWarn/notificationMode/notificationMode_page.dart'; -import 'package:star_lock/main/lockDetail/monitoring/star_chart_h264/star_chart_page.dart'; import 'package:star_lock/main/lockDetail/palm/addPalm/addPalm_page.dart'; import 'package:star_lock/main/lockDetail/palm/palmList/palmList_page.dart'; import 'package:star_lock/main/lockDetail/passwordKey/passwordKeyDetailChangeDate/passwordKeyDetailChangeDate_page.dart'; @@ -61,6 +60,7 @@ import 'package:star_lock/mine/mineSet/transferSmartLock/transferSmartLockList/t import 'package:star_lock/mine/valueAddedServices/advancedFeaturesWeb/advancedFeaturesWeb_page.dart'; import 'package:star_lock/mine/valueAddedServices/advancedFunctionRecord/advancedFunctionRecord_page.dart'; import 'package:star_lock/mine/valueAddedServices/valueAddedServicesRecord/value_added_services_record_page.dart'; +import 'package:star_lock/talk/startChart/views/talkView/talk_view_page.dart'; import 'common/safetyVerification/safetyVerification_page.dart'; import 'login/forgetPassword/starLock_forgetPassword_page.dart'; @@ -514,6 +514,7 @@ abstract class Routers { static const String googleHomePage = '/googleHomePage'; //GoogleHome static const String doubleLockLinkPage = '/doubleLockLinkPage'; //双锁联动 static const String starChartPage = '/starChartPage'; //星图 + static const String starChartTalkView = '/starChartTalkView'; //星图对讲页面 } abstract class AppRouters { @@ -747,7 +748,8 @@ abstract class AppRouters { GetPage( name: Routers.lockTimePage, page: () => const LockTimePage(), - ), // 诊断 + ), + // 诊断 GetPage( name: Routers.diagnosePage, page: () => const DiagnosePage(), @@ -1192,6 +1194,6 @@ abstract class AppRouters { name: Routers.doubleLockLinkPage, page: () => const DoubleLockLinkPage()), GetPage( - name: Routers.starChartPage, page: () => const StarChartPage()), + name: Routers.starChartTalkView, page: () => const TalkViewPage()), ]; } diff --git a/lib/blue/io_tool/manager_event_bus.dart b/lib/blue/io_tool/manager_event_bus.dart index f85b9bc7..566a1899 100755 --- a/lib/blue/io_tool/manager_event_bus.dart +++ b/lib/blue/io_tool/manager_event_bus.dart @@ -21,4 +21,12 @@ class EventBusManager { eventBusFir(dynamic event) { eventBus?.fire(event); } + + // 发送事件 + void fireEvent(dynamic event) { + eventBus?.fire(event); + } + + // 获取 EventBus 实例 + EventBus? get bus => eventBus; } diff --git a/lib/main/lockDetail/monitoring/star_chart_h264/star_chart_logic.dart b/lib/main/lockDetail/monitoring/star_chart_h264/star_chart_logic.dart deleted file mode 100644 index a5f91c0a..00000000 --- a/lib/main/lockDetail/monitoring/star_chart_h264/star_chart_logic.dart +++ /dev/null @@ -1,276 +0,0 @@ -import 'dart:async'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_pcm_sound/flutter_pcm_sound.dart'; -import 'package:flutter_voice_processor/flutter_voice_processor.dart'; -import 'package:get/get.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:star_lock/app_settings/app_settings.dart'; -import 'package:star_lock/blue/io_tool/manager_event_bus.dart'; -import 'package:star_lock/main/lockDetail/monitoring/star_chart_h264/star_chart_state.dart'; -import 'package:star_lock/talk/startChart/events/talk_status_change_event.dart'; -import 'package:star_lock/talk/startChart/handle/other/talk_data_repository.dart'; -import 'package:star_lock/talk/startChart/proto/talk_data.pbenum.dart'; -import 'package:star_lock/talk/startChart/start_chart_manage.dart'; -import 'package:star_lock/talk/startChart/start_chart_talk_status.dart'; -import 'package:star_lock/tools/baseGetXController.dart'; -import 'package:star_lock/tools/eventBusEventManage.dart'; - -class StarChartLogic extends BaseGetXController { - final StarChartState state = StarChartState(); - - /// 收到Talk发送的状态 - StreamSubscription? _getTalkStatusRefreshUIEvent; - - int startTime = DateTime.now().millisecondsSinceEpoch; - - @override - void onReady() { - super.onReady(); - // 初始化音频播放设备,确保采样率和声道数与 PCM 数据匹配 - FlutterPcmSound.setup(sampleRate: 8000, channelCount: 1); - - // 设置音频数据的供给阈值 - FlutterPcmSound.setFeedThreshold(8000 ~/ 2); // 根据需要调整 - _getTalkStatusRefreshUIAction(); - _startListenTalkData(); - } - - void _getTalkStatusRefreshUIAction() { - _getTalkStatusRefreshUIEvent = EventBusManager() - .eventBus! - .on() - .listen((TalkStatusChangeEvent event) async { - state.talkStatus.value = event.newStatus.index; - state.oneMinuteTime.value = 0; - if (state.talkStatus.value == TalkStatus.rejected.index || - state.talkStatus.value == TalkStatus.notTalkData.index || - state.talkStatus.value == TalkStatus.notTalkPing.index || - state.talkStatus.value == TalkStatus.end.index) { - _cancelTimers(); - stopProcessing(); - state.listPhotoData.value = Uint8List(0); - // 停止播放音频 - _stopPlayG711Data(); - // 状态错误,返回页面 - Get.back(); - - return; - } - - if (state.talkStatus.value == TalkStatus.duringCall.index) { - _startCallTimer(); - } - }); - } - - // 监听音视频数据流 - void _startListenTalkData() { - state.talkDataRepository.talkDataStream.listen((talkData) { - final contentType = talkData.contentType; - // 判断数据类型,进行分发处理 - switch (contentType) { - case TalkData_ContentTypeE.G711: - _playG711Data(talkData.content); - break; - case TalkData_ContentTypeE.Image: - // 收到视频数据 - state.listPhotoData.value = Uint8List.fromList(talkData.content); - break; - } - }); - } - - void syncPlay() { - int currentTime = DateTime.now().millisecondsSinceEpoch - startTime; - - // 播放音频 - // while (audioBuffer.isNotEmpty && audioBuffer.first.durationMs <= currentTime) { - // TalkData audioData = audioBuffer.removeAt(0); - // playAudio(audioData.content); - // } - // - // // 播放视频 - // while (videoBuffer.isNotEmpty && videoBuffer.first.durationMs <= currentTime) { - // TalkData videoData = videoBuffer.removeAt(0); - // playVideo(videoData.content); - // } - } - - /// 播放音频数据 - Future _playG711Data(List pcmData) async { - // 将 PCM 数据转换为 PcmArrayInt16 - final PcmArrayInt16 fromList = PcmArrayInt16.fromList(pcmData); - await FlutterPcmSound.feed(fromList); - FlutterPcmSound.play(); - } - - void _stopPlayG711Data() { - FlutterPcmSound.pause(); - FlutterPcmSound.clear(); - FlutterPcmSound.stop(); - } - - void _startCallTimer() { - if (state.oneMinuteTimeTimer.isActive) return; - - state.oneMinuteTimeTimer.cancel(); - state.oneMinuteTimeTimer = - Timer.periodic(const Duration(seconds: 1), (Timer t) { - state.oneMinuteTime.value++; - // if (state.oneMinuteTime.value >= 60) { - // t.cancel(); - // initiateHangUpCommand(); - // AppLog.log('通话时间超过60秒,自动挂断'); - // state.oneMinuteTime.value = 0; - // } - }); - } - - void _cancelTimers() { - state.oneMinuteTimeTimer.cancel(); - } - - // 发起接听命令 - void initiateAnswerCommand() { - StartChartManage().sendTalkAcceptMessage(); - } - - // 发起挂断命令 - void initiateHangUpCommand() { - _cancelTimers(); - if (state.talkStatus.value == TalkStatus.duringCall.index) { - // 如果是通话中就挂断 - StartChartManage().sendTalkHangupMessage(); - } else { - // 拒绝 - StartChartManage().sendTalkRejectMessage(); - } - Get.back(); - } - - Future _onFrame(List frame) async { - state.allFrames.add(frame); - final List concatenatedFrames = concatenateFrames(state.allFrames); - AppLog.log('pcm数据:$concatenatedFrames'); - - final List pcmBytes = listLinearToULaw(frame); - //发起发送录音命令 - // StartChartManage().sendTalkAudioMessage(pcmBytes); - } - - List listLinearToULaw(List pcmList) { - return pcmList.map(linearToULaw).toList(); - } - - List concatenateFrames(List> frames) { - return frames.expand((frame) => frame).toList(); - } - - Future startProcessing() async { - // state.isButtonDisabled.value = true; - // - // state.voiceProcessor?.addFrameListener(_onFrame); - // state.voiceProcessor?.addErrorListener(_onError); - // try { - // if (await state.voiceProcessor?.hasRecordAudioPermission() ?? false) { - // await state.voiceProcessor?.start(state.frameLength, state.sampleRate); - // state.isProcessing.value = - // await state.voiceProcessor?.isRecording() ?? false; - // } else { - // state.errorMessage.value = 'Recording permission not granted'; - // } - // } on PlatformException catch (ex) { - // state.errorMessage.value = 'Failed to start recorder: $ex'; - // } finally { - // state.isButtonDisabled.value = false; - // } - } - - void _onError(VoiceProcessorException error) { - state.errorMessage.value = error.message!; - } - - Future stopProcessing() async { - // 检查 voiceProcessor 是否已经初始化 - // if (state.voiceProcessor == null) { - // state.errorMessage.value = 'Voice processor is not initialized.'; - // return; - // } - // - // state.isButtonDisabled.value = true; - // try { - // await state.voiceProcessor?.stop(); - // state.voiceProcessor?.removeFrameListener(_onFrame); - // state.udpSendDataFrameNumber = 0; - // } on PlatformException catch (ex) { - // state.errorMessage.value = 'Failed to stop recorder: $ex'; - // } finally { - // state.isProcessing.value = - // await state.voiceProcessor?.isRecording() ?? false; - // state.isButtonDisabled.value = false; - // } - } - - int linearToULaw(int pcmVal) { - int mask; - int seg; - int uval; - - if (pcmVal < 0) { - pcmVal = 0x84 - pcmVal; - mask = 0x7F; - } else { - pcmVal += 0x84; - mask = 0xFF; - } - - seg = search(pcmVal); - if (seg >= 8) { - return 0x7F ^ mask; - } else { - uval = seg << 4; - uval |= (pcmVal >> (seg + 3)) & 0xF; - return uval ^ mask; - } - } - - int search(int val) { - final List table = [ - 0xFF, - 0x1FF, - 0x3FF, - 0x7FF, - 0xFFF, - 0x1FFF, - 0x3FFF, - 0x7FFF - ]; - for (int i = 0; i < table.length; i++) { - if (val <= table[i]) { - return i; - } - } - return table.length; - } - - Future getPermissionStatus() async { - const Permission permission = Permission.microphone; - final PermissionStatus status = await permission.status; - if (status.isGranted) { - return true; - } else if (status.isDenied || status.isRestricted) { - await requestPermission(permission); - } else if (status.isPermanentlyDenied) { - openAppSettings(); - } - return false; - } - - Future requestPermission(Permission permission) async { - final PermissionStatus status = await permission.request(); - if (status.isPermanentlyDenied) { - openAppSettings(); - } - } -} diff --git a/lib/main/lockDetail/monitoring/star_chart_h264/star_chart_page.dart b/lib/main/lockDetail/monitoring/star_chart_h264/star_chart_page.dart deleted file mode 100644 index 1717c207..00000000 --- a/lib/main/lockDetail/monitoring/star_chart_h264/star_chart_page.dart +++ /dev/null @@ -1,394 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'dart:ui' as ui; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:get/get.dart'; -import 'package:image_gallery_saver/image_gallery_saver.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:star_lock/app_settings/app_colors.dart'; -import 'package:star_lock/app_settings/app_settings.dart'; -import 'package:star_lock/main/lockDetail/monitoring/star_chart_h264/star_chart_logic.dart'; -import 'package:star_lock/main/lockDetail/monitoring/star_chart_h264/star_chart_state.dart'; -import 'package:star_lock/talk/startChart/proto/talk_data.pbenum.dart'; -import 'package:star_lock/talk/startChart/start_chart_talk_status.dart'; -import 'package:star_lock/talk/startChart/webView/h264_web_view.dart'; -import 'package:star_lock/talk/udp/udp_manage.dart'; -import 'package:star_lock/tools/eventBusEventManage.dart'; -import 'package:star_lock/tools/showTFView.dart'; - -class StarChartPage extends StatefulWidget { - const StarChartPage({Key? key}) : super(key: key); - - @override - State createState() => _StarChartPageState(); -} - -class _StarChartPageState extends State { - final StarChartLogic logic = Get.put(StarChartLogic()); - final StarChartState state = Get.find().state; - - @override - void initState() { - super.initState(); - - initAsync(); - } - - Future initAsync() async { - await requestMicrophonePermission(); - } - - @override - Widget build(BuildContext context) { - return PopScope( - canPop: false, - child: RepaintBoundary( - key: state.globalKey, - child: Container( - width: 1.sw, - height: 1.sh, - color: Colors.transparent, - child: _buildTalkView(isMpeg4: true), - ), - ), - ); - } - - Widget buildTopButtons() { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - buildIconButton( - icon: state.isOpenVoice.value - ? 'images/main/icon_lockDetail_monitoringCloseVoice.png' - : 'images/main/icon_lockDetail_monitoringOpenVoice.png', - onTap: () { - state.isOpenVoice.value = !state.isOpenVoice.value; - }, - ), - SizedBox(width: 60.w), - buildIconButton( - icon: 'images/main/icon_lockDetail_monitoringScreenshot.png', - onTap: captureAndSavePng, - ), - SizedBox(width: 60.w), - buildIconButton( - icon: 'images/main/icon_lockDetail_monitoringScreenRecording.png', - onTap: () { - // Get.toNamed(Routers.monitoringRealTimeScreenPage); - }, - ), - ], - ); - } - - Widget buildBottomButtons() { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - buildAnswerButton(), - buildIconButton( - icon: 'images/main/icon_lockDetail_hangUp.png', - label: '挂断'.tr, - color: Colors.red, - onTap: () async { - logic.initiateHangUpCommand(); - }, - ), - buildIconButton( - icon: 'images/main/icon_lockDetail_monitoringUnlock.png', - label: '开锁'.tr, - color: AppColors.mainColor, - onTap: () { - if (UDPManage().remoteUnlock == 1) { - showDeletPasswordAlertDialog(context); - } else { - logic.showToast('请在锁设置中开启远程开锁'.tr); - } - }, - ), - ], - ); - } - - Widget buildAnswerButton() { - return Obx(() { - // final bool isDuringCall = - // state.talkStatus.value == TalkStatus.duringCall.index; - return buildIconButton( - icon: state.talkStatus.value == TalkStatus.duringCall.index - ? 'images/main/icon_lockDetail_monitoringUnTalkback.png' - : 'images/main/icon_lockDetail_monitoringAnswerCalls.png', - label: state.talkStatus.value == TalkStatus.duringCall.index - ? '长按说话'.tr - : '接听'.tr, - onTap: () async { - if (state.talkStatus.value == TalkStatus.waitingAnswer.index) { - logic.initiateAnswerCommand(); - setState(() {}); - } - }, - onLongPress: () { - state.listAudioData.value = []; - logic.startProcessing(); - }, - onLongPressUp: () { - logic.stopProcessing(); - }, - ); - }); - } - - Widget buildIconButton({ - required String icon, - String? label, - Color color = Colors.white, - required Function() onTap, - Function()? onLongPress, - Function()? onLongPressUp, - }) { - final double wh = 80.w; - return GestureDetector( - onTap: onTap, - onLongPress: onLongPress, - onLongPressUp: onLongPressUp, - child: SizedBox( - height: 140.h, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - width: wh, - height: wh, - decoration: BoxDecoration( - color: color, - borderRadius: BorderRadius.circular((wh + 10.w * 2) / 2), - ), - padding: EdgeInsets.all(20.w), - child: Image.asset(icon, fit: BoxFit.fitWidth), - ), - if (label != null) ...[ - SizedBox(height: 20.w), - Expanded( - child: Text( - label, - style: TextStyle(fontSize: 20.sp, color: Colors.white), - textAlign: TextAlign.center, - ), - ), - ], - ], - ), - ), - ); - } - - void showDeletPasswordAlertDialog(BuildContext context) { - showDialog( - barrierDismissible: false, - context: context, - builder: (BuildContext context) { - return ShowTFView( - title: '请输入6位数字开锁密码'.tr, - tipTitle: '', - controller: state.passwordTF, - inputFormatters: [ - LengthLimitingTextInputFormatter(6), //限制长度 - FilteringTextInputFormatter.allow(RegExp('[0-9]')), - ], - sureClick: () async { - if (state.passwordTF.text.isEmpty) { - logic.showToast('请输入开锁密码'.tr); - return; - } - final List numbers = []; - final List lockIDData = utf8.encode(state.passwordTF.text); - numbers.addAll(lockIDData); - for (int i = 0; i < 6 - lockIDData.length; i++) { - numbers.add(0); - } - //todo: 开门暂未实现 - // logic.udpOpenDoorAction(numbers); - }, - cancelClick: () { - Get.back(); - }, - ); - }, - ); - } - - Future requestMicrophonePermission() async { - await logic.getPermissionStatus().then((bool value) async { - if (!value) { - return; - } - }); - } - - Future captureAndSavePng() async { - try { - if (state.globalKey.currentContext == null) { - AppLog.log('截图失败: 未找到当前上下文'); - return; - } - final RenderRepaintBoundary boundary = state.globalKey.currentContext! - .findRenderObject()! as RenderRepaintBoundary; - final ui.Image image = await boundary.toImage(); - final ByteData? byteData = - await image.toByteData(format: ui.ImageByteFormat.png); - if (byteData == null) { - AppLog.log('截图失败: 图像数据为空'); - return; - } - final Uint8List pngBytes = byteData.buffer.asUint8List(); - - final Directory directory = await getApplicationDocumentsDirectory(); - final String imagePath = '${directory.path}/screenshot.png'; - - final File imgFile = File(imagePath); - await imgFile.writeAsBytes(pngBytes); - - await ImageGallerySaver.saveFile(imagePath); - - AppLog.log('截图保存路径: $imagePath'); - logic.showToast('截图已保存到相册'.tr); - } catch (e) { - AppLog.log('截图失败: $e'); - } - } - - String listToHexString(List intList) { - // 将整数列表转换为十六进制字符串列表 - List hexList = intList.map((num) => num.toRadixString(16)).toList(); - // 将十六进制字符串列表连接成一个字符串,没有空格 - return hexList.join(''); - } - - @override - void dispose() { - super.dispose(); - logic.stopProcessing(); - // state.getTVDataRefreshUIEvent!.cancel(); - } - - Widget _buildTalkView({required bool isMpeg4}) { - return isMpeg4 ? _buildMpeg4TalkView() : _buildH264TalkView(); - } - - Widget _buildMpeg4TalkView() { - return Obx( - () => Stack( - children: [ - state.listPhotoData.value.isNotEmpty - ? Image.memory( - state.listPhotoData.value, - gaplessPlayback: true, - width: 1.sw, - height: 1.sh, - fit: BoxFit.cover, - filterQuality: FilterQuality.high, - errorBuilder: (BuildContext context, Object error, - StackTrace? stackTrace) { - return Container(color: Colors.transparent); - }, - ) - : Image.asset( - 'images/main/monitorBg.png', - width: 1.sw, - height: 1.sh, - fit: BoxFit.cover, - ), - Positioned( - top: ScreenUtil().statusBarHeight + 30.h, - width: 1.sw, - child: Obx(() { - final String sec = - (state.oneMinuteTime.value % 60).toString().padLeft(2, '0'); - final String min = - (state.oneMinuteTime.value ~/ 60).toString().padLeft(2, '0'); - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('$min:$sec', - style: TextStyle(fontSize: 26.sp, color: Colors.white)), - ], - ); - }), - ), - Positioned( - bottom: 10.w, - child: Container( - width: 1.sw - 30.w * 2, - margin: EdgeInsets.all(30.w), - decoration: BoxDecoration( - color: const Color(0xC83C3F41), - borderRadius: BorderRadius.circular(20.h), - ), - child: Column( - children: [ - SizedBox(height: 20.h), - buildTopButtons(), - SizedBox(height: 20.h), - buildBottomButtons(), - SizedBox(height: 20.h), - ], - ), - ), - ), - ], - ), - ); - } - - Widget _buildH264TalkView() { - return Stack( - children: [ - H264WebView(), - Positioned( - top: ScreenUtil().statusBarHeight + 30.h, - width: 1.sw, - child: Obx(() { - final String sec = - (state.oneMinuteTime.value % 60).toString().padLeft(2, '0'); - final String min = - (state.oneMinuteTime.value ~/ 60).toString().padLeft(2, '0'); - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('$min:$sec', - style: TextStyle(fontSize: 26.sp, color: Colors.white)), - ], - ); - }), - ), - Positioned( - bottom: 10.w, - child: Container( - width: 1.sw - 30.w * 2, - margin: EdgeInsets.all(30.w), - decoration: BoxDecoration( - color: const Color(0xC83C3F41), - borderRadius: BorderRadius.circular(20.h), - ), - child: Column( - children: [ - SizedBox(height: 20.h), - buildTopButtons(), - SizedBox(height: 20.h), - buildBottomButtons(), - SizedBox(height: 20.h), - ], - ), - ), - ), - ], - ); - } -} diff --git a/lib/main/lockDetail/monitoring/star_chart_h264/star_chart_state.dart b/lib/main/lockDetail/monitoring/star_chart_h264/star_chart_state.dart deleted file mode 100644 index 68268c85..00000000 --- a/lib/main/lockDetail/monitoring/star_chart_h264/star_chart_state.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:flutter/material.dart'; -import 'package:flutter_voice_processor/flutter_voice_processor.dart'; -import 'package:get/get.dart'; -import 'package:network_info_plus/network_info_plus.dart'; -import 'package:star_lock/talk/startChart/handle/other/talk_data_repository.dart'; -import 'package:star_lock/talk/startChart/start_chart_talk_status.dart'; - -import '../../../../tools/storage.dart'; - -class StarChartState { - RxBool isOpenVoice = false.obs; - int udpSendDataFrameNumber = 0; // 帧序号 - // var isSenderAudioData = false.obs;// 是否要发送音频数据 - // StreamSubscription? getTVDataRefreshUIEvent; //收到视频流数据 - RxBool shouldUpdateUI = false.obs; //是否需要更新UI - - Future userMobileIP = NetworkInfo().getWifiIP(); - Future userUid = Storage.getUid(); - - // RxInt udpStatus = - // 0.obs; //0:初始状态 1:等待监视 2: 3:监视中 4:呼叫成功 5:主角通话中 6:被叫通话 8:被叫通话中 9:长按说话 - TextEditingController passwordTF = TextEditingController(); - - Rx listPhotoData = Uint8List(0).obs; //得到的视频流字节数据 - RxList listAudioData = [].obs; //得到的音频流字节数据 - -//录音相关 - late VoiceProcessor? voiceProcessor; - RxBool isProcessing = false.obs; //是否正在处理音频数据 - RxBool isButtonDisabled = false.obs; //是否禁用按钮 - final int frameLength = 320; //音视频帧长度为320 - final int sampleRate = 8000; //音频采样率为8000 - RxString errorMessage = ''.obs; - List> allFrames = >[]; - - GlobalKey globalKey = GlobalKey(); - - late Timer oneMinuteTimeTimer = - Timer(const Duration(seconds: 1), () {}); // 定时器超过60秒关闭当前界面 - RxInt oneMinuteTime = 0.obs; // 定时器秒数 - - // 定时器如果发送了接听的命令 而没收到回复就每秒重复发送10次 - late Timer answerTimer = Timer(const Duration(seconds: 1), () {}); //接听命令定时器 - RxInt answerSeconds = 0.obs; - RxBool isClickAnswer = false.obs; //是否点击了接听按钮 - - late Timer hangUpTimer = Timer(const Duration(seconds: 1), () {}); //挂断命令定时器 - RxInt hangUpSeconds = 0.obs; - RxBool isClickHangUp = false.obs; //是否点击了挂断按钮 - - late Timer openDoorTimer = Timer(const Duration(seconds: 1), () {}); //开门命令定时器 - RxInt openDoorSeconds = 0.obs; - - RxInt talkStatus = 0.obs; //星图对讲状态 - - // 获取 StartChartTalkStatus 的唯一实例 - StartChartTalkStatus talkStatusInstance = StartChartTalkStatus.instance; - - // 通话数据流的单例流数据处理类 - final TalkDataRepository talkDataRepository = TalkDataRepository.instance; - -} diff --git a/lib/main/lockMian/lockMain/lockMain_page.dart b/lib/main/lockMian/lockMain/lockMain_page.dart index a6697b2d..ebcdd7ff 100755 --- a/lib/main/lockMian/lockMain/lockMain_page.dart +++ b/lib/main/lockMian/lockMain/lockMain_page.dart @@ -9,6 +9,8 @@ import 'package:star_lock/app_settings/app_colors.dart'; import 'package:star_lock/blue/blue_manage.dart'; import 'package:star_lock/main/lockMian/lockList/lockList_xhj_page.dart'; import 'package:star_lock/main/lockMian/lockMain/lockMain_state.dart'; +import 'package:star_lock/talk/startChart/proto/talk_data.pb.dart'; +import 'package:star_lock/talk/startChart/proto/talk_expect.pb.dart'; import 'package:star_lock/talk/startChart/proto/talk_request.pb.dart'; import 'package:star_lock/talk/startChart/proto/test.pb.dart'; import 'package:star_lock/talk/startChart/start_chart_manage.dart'; @@ -72,6 +74,10 @@ class _StarLockMainPageState extends State _initLoadDataAction(); } + String bufferToHexString(Uint8List buffer) { + return buffer.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(); + } + @override void didChangeDependencies() { super.didChangeDependencies(); diff --git a/lib/talk/startChart/command/message_command.dart b/lib/talk/startChart/command/message_command.dart index 00093af7..86184110 100644 --- a/lib/talk/startChart/command/message_command.dart +++ b/lib/talk/startChart/command/message_command.dart @@ -295,7 +295,7 @@ class MessageCommand { static List talkExpectMessage({ required String FromPeerId, required String ToPeerId, - required TalkExpect talkExpect, + required TalkExpectReq talkExpect, int? MessageId, }) { final payload = talkExpect.writeToBuffer(); diff --git a/lib/talk/startChart/constant/talk_status.dart b/lib/talk/startChart/constant/talk_status.dart new file mode 100644 index 00000000..73f43a82 --- /dev/null +++ b/lib/talk/startChart/constant/talk_status.dart @@ -0,0 +1,18 @@ + + +enum TalkStatus { + none, // 无状态 + waitingAnswer, // 等待接听 + answeredSuccessfully, // 接听成功 + waitingData, // 等待数据 + duringCall, // 通话中 + hangingUpDuring, // 通话中挂断 + rejected, // 被拒绝 + uninitialized, // 未初始化 + initializationCompleted, // 初始化完成 + notTalkData, // 暂无通话数据 + notTalkPing, // 暂无通话保持 + error, // 错误状态 + end, // 结束 +} + diff --git a/lib/talk/startChart/entity/scp_message.dart b/lib/talk/startChart/entity/scp_message.dart index f80db8a6..2820ab61 100644 --- a/lib/talk/startChart/entity/scp_message.dart +++ b/lib/talk/startChart/entity/scp_message.dart @@ -155,7 +155,7 @@ class ScpMessage { // // _log(text: 'result bytes hex: ${hexString}'); // _log( // text: - // '\n result bytes hex: ${hexString} \n payload hex: ${hexString.substring(194)}'); + // '\n result bytes hex: ${hexString} \n payload hex: ${hexString.substring(210)}'); // ProtocolFlag (4 bytes) if (bytes.length - offset >= 4) { diff --git a/lib/talk/startChart/events/talk_status_change_event.dart b/lib/talk/startChart/events/talk_status_change_event.dart deleted file mode 100644 index aea113c2..00000000 --- a/lib/talk/startChart/events/talk_status_change_event.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:star_lock/talk/startChart/start_chart_talk_status.dart'; - -class TalkStatusChangeEvent { - final TalkStatus oldStatus; - final TalkStatus newStatus; - - TalkStatusChangeEvent(this.oldStatus, this.newStatus); - - @override - String toString() { - return "TalkStatusChangeEvent: ${oldStatus.name} -> ${newStatus.name}"; - } -} diff --git a/lib/talk/startChart/handle/impl/udp_heart_beat_handler.dart b/lib/talk/startChart/handle/impl/udp_heart_beat_handler.dart index 4e6caec1..5f0a6031 100644 --- a/lib/talk/startChart/handle/impl/udp_heart_beat_handler.dart +++ b/lib/talk/startChart/handle/impl/udp_heart_beat_handler.dart @@ -43,7 +43,15 @@ class UdpHeartBeatHandler extends ScpMessageBaseHandle void handleRealTimeData(ScpMessage scpMessage) {} @override - deserializePayload({required int payloadType, required int messageType, required List byte, int? offset, int? PayloadLength, int? spTotal, int? spIndex, int? messageId}) { + deserializePayload( + {required int payloadType, + required int messageType, + required List byte, + int? offset, + int? PayloadLength, + int? spTotal, + int? spIndex, + int? messageId}) { // 心跳 HeartbeatResponse heartbeatResponse = HeartbeatResponse.fromBytes(byte); return heartbeatResponse; diff --git a/lib/talk/startChart/handle/impl/udp_talk_accept_handler.dart b/lib/talk/startChart/handle/impl/udp_talk_accept_handler.dart index a3c1fe8c..f6e932e1 100644 --- a/lib/talk/startChart/handle/impl/udp_talk_accept_handler.dart +++ b/lib/talk/startChart/handle/impl/udp_talk_accept_handler.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_pcm_sound/flutter_pcm_sound.dart'; import 'package:get/get.dart'; import 'package:star_lock/talk/startChart/constant/message_type_constant.dart'; import 'package:star_lock/talk/startChart/entity/scp_message.dart'; @@ -17,13 +18,8 @@ class UdpTalkAcceptHandler extends ScpMessageBaseHandle implements ScpMessageHandler { @override void handleReq(ScpMessage scpMessage) { - print('收到同意接听请求'); // 回复同意接听消息 - startChartManage.sendGenericRespSuccessMessage( - ToPeerId: scpMessage.FromPeerId!, - FromPeerId: scpMessage.ToPeerId!, - PayloadType: scpMessage.PayloadType!, - ); + replySuccessMessage(scpMessage); } @override @@ -32,7 +28,6 @@ class UdpTalkAcceptHandler extends ScpMessageBaseHandle final GenericResp genericResp = scpMessage.Payload; if (checkGenericRespSuccess(genericResp)) { Future.delayed(Duration(seconds: 1), () { - print('启动定时器判断'); // 启动通话保持定时器 _handleStartTalkPing(); // 启动发送预期数据请求 @@ -42,9 +37,8 @@ class UdpTalkAcceptHandler extends ScpMessageBaseHandle }); // 停止播放铃声 stopRingtone(); - // 设置状态为接听中 + // 设置状态为接听成功 talkStatus.setAnsweredSuccessfully(); - talkStatus.setWaitingData(); } } @@ -58,7 +52,7 @@ class UdpTalkAcceptHandler extends ScpMessageBaseHandle deserializePayload( {required int payloadType, required int messageType, - required List byte, + required List byte, int? offset, int? PayloadLength, int? spTotal, diff --git a/lib/talk/startChart/handle/impl/udp_talk_data_handler.dart b/lib/talk/startChart/handle/impl/udp_talk_data_handler.dart index 8d94787d..bfcdbc7d 100644 --- a/lib/talk/startChart/handle/impl/udp_talk_data_handler.dart +++ b/lib/talk/startChart/handle/impl/udp_talk_data_handler.dart @@ -1,8 +1,11 @@ import 'dart:convert'; +import 'dart:io'; import 'dart:typed_data'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:get/get.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:star_lock/app_settings/app_settings.dart'; import 'package:star_lock/talk/call/g711.dart'; import 'package:star_lock/talk/startChart/constant/message_type_constant.dart'; import 'package:star_lock/talk/startChart/entity/scp_message.dart'; @@ -32,11 +35,13 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle final TalkData talkData = scpMessage.Payload; // 处理音视频数据 _handleTalkData(talkData: talkData); - // 设置状态为接听中 - talkStatus.setDuringCall(); } } + String bufferToHexString(List buffer) { + return buffer.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(); + } + @override deserializePayload( {required int payloadType, @@ -47,9 +52,9 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle int? spTotal, int? spIndex, int? messageId}) { + // AppLog.log( + // '没有组包之前的每一个包的数据:${byte.length} messageId:$messageId spTotal:$spTotal spIndex:$spIndex PayloadLength:$PayloadLength,byte:${bufferToHexString(byte)}'); if (messageType == MessageTypeConstant.RealTimeData) { - print( - '收到音视频数据:${byte.length} messageId:$messageId spTotal:$spTotal spIndex:$spIndex PayloadLength:$PayloadLength'); // 回声测试 if (spTotal != null && spTotal > 1 && @@ -104,13 +109,15 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle } /// 处理图片数据 - void _handleVideoImage(TalkData talkData) { + void _handleVideoImage(TalkData talkData) async { final List processCompletePayload = - _processCompletePayload(Uint8List.fromList(talkData.content)); - // 循环发送每一帧的数据 + await _processCompletePayload(Uint8List.fromList(talkData.content)); + // AppLog.log('得到完整的帧:${processCompletePayload.length}'); // 循环发送每一帧的数据 processCompletePayload.forEach((element) { talkData.content = element; talkDataRepository.addTalkData(talkData); + // 设置状态为接听中 + talkStatus.setDuringCall(); }); } @@ -122,26 +129,30 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle List pcmBytes = G711().convertList(g711Data); talkData.content = pcmBytes; talkDataRepository.addTalkData(talkData); + // 设置状态为接听中 + talkStatus.setDuringCall(); } catch (e) { print('Error decoding G.711 to PCM: $e'); } } - /// 查找完整的帧数据 - List _processCompletePayload(Uint8List payload) { + Future> _processCompletePayload(Uint8List payload) async { // 存储找到的所有完整帧 List frames = []; // 寻找完整帧 (0xFFD8 开始, 0xFFD9 结束) - int startIdx = payload.indexOf(0xFF); - while (startIdx != -1 && startIdx + 1 < payload.length) { + int startIdx = 0; + while (startIdx < payload.length - 1) { + // 找到帧的起始标志 0xFFD8 + startIdx = payload.indexOf(0xFF, startIdx); + if (startIdx == -1 || startIdx + 1 >= payload.length) break; if (payload[startIdx + 1] == 0xD8) { // 找到帧的起始标志 0xFFD8 int endIdx = startIdx + 2; while (endIdx < payload.length - 1) { endIdx = payload.indexOf(0xFF, endIdx); - if (endIdx == -1) break; - if (endIdx + 1 < payload.length && payload[endIdx + 1] == 0xD9) { + if (endIdx == -1 || endIdx + 1 >= payload.length) break; + if (payload[endIdx + 1] == 0xD9) { // 找到帧的结束标志 0xFFD9 Uint8List frame = payload.sublist(startIdx, endIdx + 2); frames.add(frame); @@ -152,7 +163,7 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle } } } else { - startIdx = payload.indexOf(0xFF, startIdx + 1); // 寻找下一个起始标志 + startIdx += 1; // 继续寻找下一个起始标志 } } diff --git a/lib/talk/startChart/handle/impl/udp_talk_expect_handler.dart b/lib/talk/startChart/handle/impl/udp_talk_expect_handler.dart index 6bdf44e7..69469f47 100644 --- a/lib/talk/startChart/handle/impl/udp_talk_expect_handler.dart +++ b/lib/talk/startChart/handle/impl/udp_talk_expect_handler.dart @@ -20,7 +20,7 @@ class UdpTalkExpectHandler extends ScpMessageBaseHandle @override void handleReq(ScpMessage scpMessage) { // 收到预期音视频数据请求 - final TalkExpect talkExpect = scpMessage.Payload; + final TalkExpectReq talkExpect = scpMessage.Payload; print('收到预期音视频数据请求:$talkExpect'); // 回复请求 @@ -49,18 +49,21 @@ class UdpTalkExpectHandler extends ScpMessageBaseHandle deserializePayload( {required int payloadType, required int messageType, - required List byte, + required List byte, int? offset, int? PayloadLength, int? spTotal, int? spIndex, int? messageId}) { if (messageType == MessageTypeConstant.Resp) { + // final TalkExpectResp talkExpectResp = TalkExpectResp(); + // talkExpectResp.mergeFromBuffer(byte); + // return talkExpectResp; final GenericResp genericResp = GenericResp(); genericResp.mergeFromBuffer(byte); return genericResp; } else if (messageType == MessageTypeConstant.Req) { - final TalkExpect talkExpect = TalkExpect(); + final TalkExpectReq talkExpect = TalkExpectReq(); talkExpect.mergeFromBuffer(byte); return talkExpect; } else { diff --git a/lib/talk/startChart/handle/impl/udp_talk_hangup_handler.dart b/lib/talk/startChart/handle/impl/udp_talk_hangup_handler.dart index dfe227ef..f7e39e2a 100644 --- a/lib/talk/startChart/handle/impl/udp_talk_hangup_handler.dart +++ b/lib/talk/startChart/handle/impl/udp_talk_hangup_handler.dart @@ -1,24 +1,25 @@ import 'dart:convert'; -import 'dart:typed_data'; -import 'package:flutter_easyloading/flutter_easyloading.dart'; -import 'package:get/get.dart'; import 'package:star_lock/talk/startChart/constant/message_type_constant.dart'; +import 'package:star_lock/talk/startChart/constant/talk_status.dart'; import 'package:star_lock/talk/startChart/entity/scp_message.dart'; import 'package:star_lock/talk/startChart/handle/scp_message_base_handle.dart'; import 'package:star_lock/talk/startChart/handle/scp_message_handle.dart'; -import 'package:star_lock/talk/startChart/proto/gateway_reset.pb.dart'; + import 'package:star_lock/talk/startChart/proto/generic.pb.dart'; import 'package:star_lock/talk/startChart/proto/talk_hangup.pb.dart'; + import '../../start_chart_manage.dart'; class UdpTalkHangUpHandler extends ScpMessageBaseHandle implements ScpMessageHandler { @override void handleReq(ScpMessage scpMessage) { - // 通话中挂断请求 - print('收到通话中挂断请求'); + if (talkStatus.status != TalkStatus.duringCall) { + // 如果不是接听中,不处理通话中挂断请求 + return; + } startChartManage.sendGenericRespSuccessMessage( ToPeerId: scpMessage.FromPeerId!, FromPeerId: scpMessage.ToPeerId!, @@ -28,18 +29,15 @@ class UdpTalkHangUpHandler extends ScpMessageBaseHandle startChartManage.stopTalkPingMessageTimer(); startChartManage.stopTalkExpectMessageTimer(); talkStatus.setHangingUpDuring(); - talkStatus.setEnd(); stopRingtone(); } @override void handleResp(ScpMessage scpMessage) { - print('收到通话中挂断回复'); // 停止发送通话保持的命令 startChartManage.stopTalkPingMessageTimer(); startChartManage.stopTalkExpectMessageTimer(); talkStatus.setHangingUpDuring(); - talkStatus.setEnd(); stopRingtone(); } @@ -53,7 +51,7 @@ class UdpTalkHangUpHandler extends ScpMessageBaseHandle deserializePayload( {required int payloadType, required int messageType, - required List byte, + required List byte, int? offset, int? PayloadLength, int? spTotal, diff --git a/lib/talk/startChart/handle/impl/udp_talk_reject_handler.dart b/lib/talk/startChart/handle/impl/udp_talk_reject_handler.dart index 96e722ac..07dfe4f6 100644 --- a/lib/talk/startChart/handle/impl/udp_talk_reject_handler.dart +++ b/lib/talk/startChart/handle/impl/udp_talk_reject_handler.dart @@ -25,19 +25,16 @@ class UdpTalkRejectHandler extends ScpMessageBaseHandle ); startChartManage.stopTalkPingMessageTimer(); startChartManage.stopTalkExpectMessageTimer(); - talkStatus.setRejected(); stopRingtone(); - talkStatus.setEnd(); + // 收到接听拒绝回复 + talkStatus.setRejected(); } @override void handleResp(ScpMessage scpMessage) { - // 收到接听拒绝回复 - talkStatus.setRejected(); startChartManage.stopTalkPingMessageTimer(); startChartManage.stopTalkExpectMessageTimer(); stopRingtone(); - talkStatus.setEnd(); } @override @@ -50,7 +47,7 @@ class UdpTalkRejectHandler extends ScpMessageBaseHandle deserializePayload( {required int payloadType, required int messageType, - required List byte, + required List byte, int? offset, int? PayloadLength, int? spTotal, diff --git a/lib/talk/startChart/handle/impl/udp_talk_request_handler.dart b/lib/talk/startChart/handle/impl/udp_talk_request_handler.dart index af67d4b6..20e649fa 100644 --- a/lib/talk/startChart/handle/impl/udp_talk_request_handler.dart +++ b/lib/talk/startChart/handle/impl/udp_talk_request_handler.dart @@ -1,22 +1,18 @@ import 'dart:convert'; -import 'dart:typed_data'; import 'package:flutter/services.dart'; -import 'package:flutter_easyloading/flutter_easyloading.dart'; + import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:get/get.dart'; import 'package:star_lock/appRouters.dart'; import 'package:star_lock/talk/startChart/constant/message_type_constant.dart'; +import 'package:star_lock/talk/startChart/constant/talk_status.dart'; import 'package:star_lock/talk/startChart/entity/scp_message.dart'; import 'package:star_lock/talk/startChart/handle/scp_message_base_handle.dart'; import 'package:star_lock/talk/startChart/handle/scp_message_handle.dart'; import 'package:star_lock/talk/startChart/proto/gateway_reset.pb.dart'; import 'package:star_lock/talk/startChart/proto/generic.pb.dart'; import 'package:star_lock/talk/startChart/proto/talk_request.pb.dart'; -import 'package:star_lock/talk/startChart/start_chart_talk_status.dart'; -import 'package:star_lock/tools/storage.dart'; - -import '../../start_chart_manage.dart'; class UdpTalkRequestHandler extends ScpMessageBaseHandle implements ScpMessageHandler { @@ -38,8 +34,6 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle startChartManage.ToPeerId = scpMessage.FromPeerId!; // 处理收到接听请求后的事件 _talkRequestEvent(talkObjectName: talkReq.callerName); - // 设置为等待接听状态 - talkStatus.setWaitingAnswer(); } @override @@ -63,11 +57,12 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle _showTalkRequestNotification(talkObjectName: talkObjectName); // 收到呼叫请求,跳转到接听页面 Get.toNamed( - Routers.starChartPage, - arguments: {'lockId': '111'}, + Routers.starChartTalkView, ); // 触发震动反馈 HapticFeedback.vibrate(); + // 设置为等待接听状态 + talkStatus.setWaitingAnswer(); } // 收到来电请求时进行本地通知 @@ -95,7 +90,7 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle deserializePayload( {required int payloadType, required int messageType, - required List byte, + required List byte, int? offset, int? PayloadLength, int? spTotal, diff --git a/lib/talk/startChart/handle/other/talk_data_repository.dart b/lib/talk/startChart/handle/other/talk_data_repository.dart index a86382c7..8eedbc3f 100644 --- a/lib/talk/startChart/handle/other/talk_data_repository.dart +++ b/lib/talk/startChart/handle/other/talk_data_repository.dart @@ -19,7 +19,7 @@ class TalkDataRepository { Stream get talkDataStream => _talkDataStreamController.stream; // 提供一个方法来添加 TalkData 到 Stream - void addTalkData(TalkData talkData) { + void addTalkData(TalkData talkData) async { _talkDataStreamController.add(talkData); } diff --git a/lib/talk/startChart/handle/scp_message_base_handle.dart b/lib/talk/startChart/handle/scp_message_base_handle.dart index de51229b..63e639dd 100644 --- a/lib/talk/startChart/handle/scp_message_base_handle.dart +++ b/lib/talk/startChart/handle/scp_message_base_handle.dart @@ -31,16 +31,19 @@ class ScpMessageBaseHandle { // 通话数据流的单例流数据处理类 final TalkDataRepository talkDataRepository = TalkDataRepository.instance; + // 获取 StartChartTalkStatus 的唯一实例 + final StartChartTalkStatus talkStatus = StartChartTalkStatus.instance; + final audioManager = AudioPlayerManager(); // 通话保持超时监听定时器管理 final talkePingOverTimeTimerManager = OverTimeTimerManager( - timeoutInSeconds: 55, + timeoutInSeconds: 260, ); // 通话数据超时定时器 final talkDataOverTimeTimerManager = OverTimeTimerManager( - timeoutInSeconds: 53, + timeoutInSeconds: 260, ); // 回复成功消息 @@ -52,9 +55,6 @@ class ScpMessageBaseHandle { ); } - // 获取 StartChartTalkStatus 的唯一实例 - StartChartTalkStatus talkStatus = StartChartTalkStatus.instance; - bool checkGenericRespSuccess(GenericResp genericResp) { if (genericResp == null) return false; final code = genericResp.code; @@ -95,8 +95,8 @@ class ScpMessageBaseHandle { // 检查分包索引是否在合法范围内 if (spIndex < 1 || spIndex > spTotal) { - print( - 'Invalid spTotal: $spTotal spIndex: $spIndex for messageId: $messageId'); + // print( + // 'Invalid spTotal: $spTotal spIndex: $spIndex for messageId: $messageId'); return null; } diff --git a/lib/talk/startChart/proto/talk_data_h264_frame.proto b/lib/talk/startChart/proto/talk_data_h264_frame.proto new file mode 100644 index 00000000..09815d16 --- /dev/null +++ b/lib/talk/startChart/proto/talk_data_h264_frame.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; +package main; +option go_package = "./spb/talk"; + +message TalkDataH264Frame { + // 帧序号 seq + uint32 FrameSeq = 1; + // 帧类型,对于H264,I帧、P帧, + // 未知为NONE?有时候发送方不知道,或者渲染方可以从帧数据中解析 + enum FrameTypeE { + NONE = 0; + I = 1; + P = 2; + }; + FrameTypeE FrameType = 2; + // 帧数据 + bytes FrameData = 3; +} diff --git a/lib/talk/startChart/proto/talk_expect.pb.dart b/lib/talk/startChart/proto/talk_expect.pb.dart index 2f624153..835baa3c 100644 --- a/lib/talk/startChart/proto/talk_expect.pb.dart +++ b/lib/talk/startChart/proto/talk_expect.pb.dart @@ -17,10 +17,11 @@ import 'talk_expect.pbenum.dart'; export 'talk_expect.pbenum.dart'; -class TalkExpect extends $pb.GeneratedMessage { - factory TalkExpect({ - $core.Iterable? videoType, - $core.Iterable? audioType, +/// 预期接收为渲染方发送,含义为:“我可以理解这些格式,你看看你方便提供什么格式” +class TalkExpectReq extends $pb.GeneratedMessage { + factory TalkExpectReq({ + $core.Iterable? videoType, + $core.Iterable? audioType, }) { final $result = create(); if (videoType != null) { @@ -31,13 +32,13 @@ class TalkExpect extends $pb.GeneratedMessage { } return $result; } - TalkExpect._() : super(); - factory TalkExpect.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); - factory TalkExpect.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + TalkExpectReq._() : super(); + factory TalkExpectReq.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory TalkExpectReq.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); - static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'TalkExpect', package: const $pb.PackageName(_omitMessageNames ? '' : 'main'), createEmptyInstance: create) - ..pc(1, _omitFieldNames ? '' : 'VideoType', $pb.PbFieldType.KE, protoName: 'VideoType', valueOf: TalkExpect_VideoTypeE.valueOf, enumValues: TalkExpect_VideoTypeE.values, defaultEnumValue: TalkExpect_VideoTypeE.NONE_V) - ..pc(2, _omitFieldNames ? '' : 'AudioType', $pb.PbFieldType.KE, protoName: 'AudioType', valueOf: TalkExpect_AudioTypeE.valueOf, enumValues: TalkExpect_AudioTypeE.values, defaultEnumValue: TalkExpect_AudioTypeE.NONE_A) + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'TalkExpectReq', package: const $pb.PackageName(_omitMessageNames ? '' : 'main'), createEmptyInstance: create) + ..pc(1, _omitFieldNames ? '' : 'VideoType', $pb.PbFieldType.KE, protoName: 'VideoType', valueOf: VideoTypeE.valueOf, enumValues: VideoTypeE.values, defaultEnumValue: VideoTypeE.NONE_V) + ..pc(2, _omitFieldNames ? '' : 'AudioType', $pb.PbFieldType.KE, protoName: 'AudioType', valueOf: AudioTypeE.valueOf, enumValues: AudioTypeE.values, defaultEnumValue: AudioTypeE.NONE_A) ..hasRequiredFields = false ; @@ -45,30 +46,141 @@ class TalkExpect extends $pb.GeneratedMessage { 'Using this can add significant overhead to your binary. ' 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' 'Will be removed in next major version') - TalkExpect clone() => TalkExpect()..mergeFromMessage(this); + TalkExpectReq clone() => TalkExpectReq()..mergeFromMessage(this); @$core.Deprecated( 'Using this can add significant overhead to your binary. ' 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' 'Will be removed in next major version') - TalkExpect copyWith(void Function(TalkExpect) updates) => super.copyWith((message) => updates(message as TalkExpect)) as TalkExpect; + TalkExpectReq copyWith(void Function(TalkExpectReq) updates) => super.copyWith((message) => updates(message as TalkExpectReq)) as TalkExpectReq; $pb.BuilderInfo get info_ => _i; @$core.pragma('dart2js:noInline') - static TalkExpect create() => TalkExpect._(); - TalkExpect createEmptyInstance() => create(); - static $pb.PbList createRepeated() => $pb.PbList(); + static TalkExpectReq create() => TalkExpectReq._(); + TalkExpectReq createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); @$core.pragma('dart2js:noInline') - static TalkExpect getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); - static TalkExpect? _defaultInstance; + static TalkExpectReq getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static TalkExpectReq? _defaultInstance; /// 如果接收到NONE的话,意味着对方关闭了喇叭或者视频显示,发送方应该停止发送 /// 支持的类型是数组 @$pb.TagNumber(1) - $core.List get videoType => $_getList(0); + $core.List get videoType => $_getList(0); @$pb.TagNumber(2) - $core.List get audioType => $_getList(1); + $core.List get audioType => $_getList(1); +} + +/// 这是音视频提供方的回应,含义为:“马上我就会为你发送这个格式的音视频,你准备好解析器吧” +class TalkExpectResp extends $pb.GeneratedMessage { + factory TalkExpectResp({ + $core.int? width, + $core.int? height, + $core.int? rotate, + VideoTypeE? videoType, + AudioTypeE? audioType, + }) { + final $result = create(); + if (width != null) { + $result.width = width; + } + if (height != null) { + $result.height = height; + } + if (rotate != null) { + $result.rotate = rotate; + } + if (videoType != null) { + $result.videoType = videoType; + } + if (audioType != null) { + $result.audioType = audioType; + } + return $result; + } + TalkExpectResp._() : super(); + factory TalkExpectResp.fromBuffer($core.List<$core.int> i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromBuffer(i, r); + factory TalkExpectResp.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); + + static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'TalkExpectResp', package: const $pb.PackageName(_omitMessageNames ? '' : 'main'), createEmptyInstance: create) + ..a<$core.int>(1, _omitFieldNames ? '' : 'Width', $pb.PbFieldType.OU3, protoName: 'Width') + ..a<$core.int>(2, _omitFieldNames ? '' : 'Height', $pb.PbFieldType.OU3, protoName: 'Height') + ..a<$core.int>(3, _omitFieldNames ? '' : 'Rotate', $pb.PbFieldType.OU3, protoName: 'Rotate') + ..e(4, _omitFieldNames ? '' : 'VideoType', $pb.PbFieldType.OE, protoName: 'VideoType', defaultOrMaker: VideoTypeE.NONE_V, valueOf: VideoTypeE.valueOf, enumValues: VideoTypeE.values) + ..e(5, _omitFieldNames ? '' : 'AudioType', $pb.PbFieldType.OE, protoName: 'AudioType', defaultOrMaker: AudioTypeE.NONE_A, valueOf: AudioTypeE.valueOf, enumValues: AudioTypeE.values) + ..hasRequiredFields = false + ; + + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version') + TalkExpectResp clone() => TalkExpectResp()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version') + TalkExpectResp copyWith(void Function(TalkExpectResp) updates) => super.copyWith((message) => updates(message as TalkExpectResp)) as TalkExpectResp; + + $pb.BuilderInfo get info_ => _i; + + @$core.pragma('dart2js:noInline') + static TalkExpectResp create() => TalkExpectResp._(); + TalkExpectResp createEmptyInstance() => create(); + static $pb.PbList createRepeated() => $pb.PbList(); + @$core.pragma('dart2js:noInline') + static TalkExpectResp getDefault() => _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static TalkExpectResp? _defaultInstance; + + /// 宽度,单位像素 + @$pb.TagNumber(1) + $core.int get width => $_getIZ(0); + @$pb.TagNumber(1) + set width($core.int v) { $_setUnsignedInt32(0, v); } + @$pb.TagNumber(1) + $core.bool hasWidth() => $_has(0); + @$pb.TagNumber(1) + void clearWidth() => clearField(1); + + /// 高度,单位像素 + @$pb.TagNumber(2) + $core.int get height => $_getIZ(1); + @$pb.TagNumber(2) + set height($core.int v) { $_setUnsignedInt32(1, v); } + @$pb.TagNumber(2) + $core.bool hasHeight() => $_has(1); + @$pb.TagNumber(2) + void clearHeight() => clearField(2); + + /// 旋转角度 默认0 + @$pb.TagNumber(3) + $core.int get rotate => $_getIZ(2); + @$pb.TagNumber(3) + set rotate($core.int v) { $_setUnsignedInt32(2, v); } + @$pb.TagNumber(3) + $core.bool hasRotate() => $_has(2); + @$pb.TagNumber(3) + void clearRotate() => clearField(3); + + /// 即将发送的音视频格式 + @$pb.TagNumber(4) + VideoTypeE get videoType => $_getN(3); + @$pb.TagNumber(4) + set videoType(VideoTypeE v) { setField(4, v); } + @$pb.TagNumber(4) + $core.bool hasVideoType() => $_has(3); + @$pb.TagNumber(4) + void clearVideoType() => clearField(4); + + @$pb.TagNumber(5) + AudioTypeE get audioType => $_getN(4); + @$pb.TagNumber(5) + set audioType(AudioTypeE v) { setField(5, v); } + @$pb.TagNumber(5) + $core.bool hasAudioType() => $_has(4); + @$pb.TagNumber(5) + void clearAudioType() => clearField(5); } diff --git a/lib/talk/startChart/proto/talk_expect.pbenum.dart b/lib/talk/startChart/proto/talk_expect.pbenum.dart index ef51cbcd..d6b34250 100644 --- a/lib/talk/startChart/proto/talk_expect.pbenum.dart +++ b/lib/talk/startChart/proto/talk_expect.pbenum.dart @@ -14,39 +14,43 @@ import 'dart:core' as $core; import 'package:protobuf/protobuf.dart' as $pb; /// 视频类型 -class TalkExpect_VideoTypeE extends $pb.ProtobufEnum { - static const TalkExpect_VideoTypeE NONE_V = TalkExpect_VideoTypeE._(0, _omitEnumNames ? '' : 'NONE_V'); - static const TalkExpect_VideoTypeE H264 = TalkExpect_VideoTypeE._(1, _omitEnumNames ? '' : 'H264'); - static const TalkExpect_VideoTypeE IMAGE = TalkExpect_VideoTypeE._(2, _omitEnumNames ? '' : 'IMAGE'); +class VideoTypeE extends $pb.ProtobufEnum { + static const VideoTypeE NONE_V = VideoTypeE._(0, _omitEnumNames ? '' : 'NONE_V'); + static const VideoTypeE H264 = VideoTypeE._(1, _omitEnumNames ? '' : 'H264'); + static const VideoTypeE IMAGE = VideoTypeE._(2, _omitEnumNames ? '' : 'IMAGE'); + static const VideoTypeE VP8 = VideoTypeE._(3, _omitEnumNames ? '' : 'VP8'); - static const $core.List values = [ + static const $core.List values = [ NONE_V, H264, IMAGE, + VP8, ]; - static final $core.Map<$core.int, TalkExpect_VideoTypeE> _byValue = $pb.ProtobufEnum.initByValue(values); - static TalkExpect_VideoTypeE? valueOf($core.int value) => _byValue[value]; + static final $core.Map<$core.int, VideoTypeE> _byValue = $pb.ProtobufEnum.initByValue(values); + static VideoTypeE? valueOf($core.int value) => _byValue[value]; - const TalkExpect_VideoTypeE._($core.int v, $core.String n) : super(v, n); + const VideoTypeE._($core.int v, $core.String n) : super(v, n); } /// 音频类型 -class TalkExpect_AudioTypeE extends $pb.ProtobufEnum { - static const TalkExpect_AudioTypeE NONE_A = TalkExpect_AudioTypeE._(0, _omitEnumNames ? '' : 'NONE_A'); - static const TalkExpect_AudioTypeE AAC = TalkExpect_AudioTypeE._(1, _omitEnumNames ? '' : 'AAC'); - static const TalkExpect_AudioTypeE G711 = TalkExpect_AudioTypeE._(2, _omitEnumNames ? '' : 'G711'); +class AudioTypeE extends $pb.ProtobufEnum { + static const AudioTypeE NONE_A = AudioTypeE._(0, _omitEnumNames ? '' : 'NONE_A'); + static const AudioTypeE AAC = AudioTypeE._(1, _omitEnumNames ? '' : 'AAC'); + static const AudioTypeE G711 = AudioTypeE._(2, _omitEnumNames ? '' : 'G711'); + static const AudioTypeE OPUS = AudioTypeE._(3, _omitEnumNames ? '' : 'OPUS'); - static const $core.List values = [ + static const $core.List values = [ NONE_A, AAC, G711, + OPUS, ]; - static final $core.Map<$core.int, TalkExpect_AudioTypeE> _byValue = $pb.ProtobufEnum.initByValue(values); - static TalkExpect_AudioTypeE? valueOf($core.int value) => _byValue[value]; + static final $core.Map<$core.int, AudioTypeE> _byValue = $pb.ProtobufEnum.initByValue(values); + static AudioTypeE? valueOf($core.int value) => _byValue[value]; - const TalkExpect_AudioTypeE._($core.int v, $core.String n) : super(v, n); + const AudioTypeE._($core.int v, $core.String n) : super(v, n); } diff --git a/lib/talk/startChart/proto/talk_expect.pbjson.dart b/lib/talk/startChart/proto/talk_expect.pbjson.dart index 61ace4ca..89cfcd51 100644 --- a/lib/talk/startChart/proto/talk_expect.pbjson.dart +++ b/lib/talk/startChart/proto/talk_expect.pbjson.dart @@ -13,40 +13,66 @@ import 'dart:convert' as $convert; import 'dart:core' as $core; import 'dart:typed_data' as $typed_data; -@$core.Deprecated('Use talkExpectDescriptor instead') -const TalkExpect$json = { - '1': 'TalkExpect', - '2': [ - {'1': 'VideoType', '3': 1, '4': 3, '5': 14, '6': '.main.TalkExpect.VideoTypeE', '10': 'VideoType'}, - {'1': 'AudioType', '3': 2, '4': 3, '5': 14, '6': '.main.TalkExpect.AudioTypeE', '10': 'AudioType'}, - ], - '4': [TalkExpect_VideoTypeE$json, TalkExpect_AudioTypeE$json], -}; - -@$core.Deprecated('Use talkExpectDescriptor instead') -const TalkExpect_VideoTypeE$json = { +@$core.Deprecated('Use videoTypeEDescriptor instead') +const VideoTypeE$json = { '1': 'VideoTypeE', '2': [ {'1': 'NONE_V', '2': 0}, {'1': 'H264', '2': 1}, {'1': 'IMAGE', '2': 2}, + {'1': 'VP8', '2': 3}, ], }; -@$core.Deprecated('Use talkExpectDescriptor instead') -const TalkExpect_AudioTypeE$json = { +/// Descriptor for `VideoTypeE`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List videoTypeEDescriptor = $convert.base64Decode( + 'CgpWaWRlb1R5cGVFEgoKBk5PTkVfVhAAEggKBEgyNjQQARIJCgVJTUFHRRACEgcKA1ZQOBAD'); + +@$core.Deprecated('Use audioTypeEDescriptor instead') +const AudioTypeE$json = { '1': 'AudioTypeE', '2': [ {'1': 'NONE_A', '2': 0}, {'1': 'AAC', '2': 1}, {'1': 'G711', '2': 2}, + {'1': 'OPUS', '2': 3}, ], }; -/// Descriptor for `TalkExpect`. Decode as a `google.protobuf.DescriptorProto`. -final $typed_data.Uint8List talkExpectDescriptor = $convert.base64Decode( - 'CgpUYWxrRXhwZWN0EjkKCVZpZGVvVHlwZRgBIAMoDjIbLm1haW4uVGFsa0V4cGVjdC5WaWRlb1' - 'R5cGVFUglWaWRlb1R5cGUSOQoJQXVkaW9UeXBlGAIgAygOMhsubWFpbi5UYWxrRXhwZWN0LkF1' - 'ZGlvVHlwZUVSCUF1ZGlvVHlwZSItCgpWaWRlb1R5cGVFEgoKBk5PTkVfVhAAEggKBEgyNjQQAR' - 'IJCgVJTUFHRRACIisKCkF1ZGlvVHlwZUUSCgoGTk9ORV9BEAASBwoDQUFDEAESCAoERzcxMRAC'); +/// Descriptor for `AudioTypeE`. Decode as a `google.protobuf.EnumDescriptorProto`. +final $typed_data.Uint8List audioTypeEDescriptor = $convert.base64Decode( + 'CgpBdWRpb1R5cGVFEgoKBk5PTkVfQRAAEgcKA0FBQxABEggKBEc3MTEQAhIICgRPUFVTEAM='); + +@$core.Deprecated('Use talkExpectReqDescriptor instead') +const TalkExpectReq$json = { + '1': 'TalkExpectReq', + '2': [ + {'1': 'VideoType', '3': 1, '4': 3, '5': 14, '6': '.main.VideoTypeE', '10': 'VideoType'}, + {'1': 'AudioType', '3': 2, '4': 3, '5': 14, '6': '.main.AudioTypeE', '10': 'AudioType'}, + ], +}; + +/// Descriptor for `TalkExpectReq`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List talkExpectReqDescriptor = $convert.base64Decode( + 'Cg1UYWxrRXhwZWN0UmVxEi4KCVZpZGVvVHlwZRgBIAMoDjIQLm1haW4uVmlkZW9UeXBlRVIJVm' + 'lkZW9UeXBlEi4KCUF1ZGlvVHlwZRgCIAMoDjIQLm1haW4uQXVkaW9UeXBlRVIJQXVkaW9UeXBl'); + +@$core.Deprecated('Use talkExpectRespDescriptor instead') +const TalkExpectResp$json = { + '1': 'TalkExpectResp', + '2': [ + {'1': 'Width', '3': 1, '4': 1, '5': 13, '10': 'Width'}, + {'1': 'Height', '3': 2, '4': 1, '5': 13, '10': 'Height'}, + {'1': 'Rotate', '3': 3, '4': 1, '5': 13, '10': 'Rotate'}, + {'1': 'VideoType', '3': 4, '4': 1, '5': 14, '6': '.main.VideoTypeE', '10': 'VideoType'}, + {'1': 'AudioType', '3': 5, '4': 1, '5': 14, '6': '.main.AudioTypeE', '10': 'AudioType'}, + ], +}; + +/// Descriptor for `TalkExpectResp`. Decode as a `google.protobuf.DescriptorProto`. +final $typed_data.Uint8List talkExpectRespDescriptor = $convert.base64Decode( + 'Cg5UYWxrRXhwZWN0UmVzcBIUCgVXaWR0aBgBIAEoDVIFV2lkdGgSFgoGSGVpZ2h0GAIgASgNUg' + 'ZIZWlnaHQSFgoGUm90YXRlGAMgASgNUgZSb3RhdGUSLgoJVmlkZW9UeXBlGAQgASgOMhAubWFp' + 'bi5WaWRlb1R5cGVFUglWaWRlb1R5cGUSLgoJQXVkaW9UeXBlGAUgASgOMhAubWFpbi5BdWRpb1' + 'R5cGVFUglBdWRpb1R5cGU='); diff --git a/lib/talk/startChart/proto/talk_expect.proto b/lib/talk/startChart/proto/talk_expect.proto index c7ee69d5..9a73d13f 100644 --- a/lib/talk/startChart/proto/talk_expect.proto +++ b/lib/talk/startChart/proto/talk_expect.proto @@ -3,21 +3,39 @@ syntax = "proto3"; package main; option go_package = "./spb/talk"; -message TalkExpect { - // 视频类型 - enum VideoTypeE { - NONE_V = 0; - H264 = 1; - IMAGE = 2; - } - // 音频类型 - enum AudioTypeE { - NONE_A = 0; - AAC = 1; - G711 = 2; - } +// 视频类型 +enum VideoTypeE { + NONE_V = 0; + H264 = 1; // 也称为 AVC,采用了多种高效的编码技术,如帧间压缩、运动估计、变换编码等,能够在保持较高画质的同时减小文件大小,具有广泛的兼容性和硬件支持 + IMAGE = 2; // 一种简单的视频压缩格式,它将每一帧视频都压缩成单独的JPEG图像帧 经常用于低延迟的视频流应用 如网络摄像头、数字视频录像机、监控视频。它也常用于一些老旧的设备中。 + VP8 = 3; // 由Google开发,开源,软件编码器,WebRTC默认编码器 广泛用于WebRTC视频会议、YouTube视频播放等。VP8 已被Google广泛推广,尤其在HTML5视频和实时视频应用中有着广泛应用。 +} +// 音频类型 +enum AudioTypeE { + NONE_A = 0; + AAC = 1; // AAC 是一种有损压缩音频编解码器,广泛应用于多媒体和流媒体传输。 + G711 = 2; // 无损压缩算法,使用8 kHz的采样率,每个采样点使用8位数据。虽然压缩效率低但非常高的音质,广泛应用于电话和VoIP中。 + OPUS = 3; // 开源,WebRTC默认编码器 低延迟、广泛的比特率范围(从6 kbps到510 kbps)以及适应性强的特性,特别适合用于实时通信(如VoIP)和高质量的音乐流媒体。 +} + +// 预期接收为渲染方发送,含义为:“我可以理解这些格式,你看看你方便提供什么格式” +message TalkExpectReq { // 如果接收到NONE的话,意味着对方关闭了喇叭或者视频显示,发送方应该停止发送 // 支持的类型是数组 repeated VideoTypeE VideoType = 1; repeated AudioTypeE AudioType = 2; } + +// 这是音视频提供方的回应,含义为:“马上我就会为你发送这个格式的音视频,你准备好解析器吧” +message TalkExpectResp { + // 宽度,单位像素 + uint32 Width = 1; + // 高度,单位像素 + uint32 Height = 2; + // 旋转角度 默认0 + uint32 Rotate = 3; + + // 即将发送的音视频格式 + VideoTypeE VideoType = 4; + AudioTypeE AudioType = 5; +} diff --git a/lib/talk/startChart/start_chart_manage.dart b/lib/talk/startChart/start_chart_manage.dart index e7de70a6..83485db6 100644 --- a/lib/talk/startChart/start_chart_manage.dart +++ b/lib/talk/startChart/start_chart_manage.dart @@ -12,11 +12,13 @@ import 'package:star_lock/flavors.dart'; import 'package:star_lock/login/login/entity/LoginEntity.dart'; import 'package:star_lock/network/api_repository.dart'; import 'package:star_lock/network/start_chart_api.dart'; +import 'package:star_lock/talk/other/audio_player_manager.dart'; import 'package:star_lock/talk/startChart/command/message_command.dart'; import 'package:star_lock/talk/startChart/constant/ip_constant.dart'; import 'package:star_lock/talk/startChart/constant/listen_addr_type_constant.dart'; import 'package:star_lock/talk/startChart/constant/message_type_constant.dart'; import 'package:star_lock/talk/startChart/constant/payload_type_constant.dart'; +import 'package:star_lock/talk/startChart/constant/talk_status.dart'; import 'package:star_lock/talk/startChart/entity/relay_info_entity.dart'; import 'package:star_lock/talk/startChart/entity/report_information_data.dart'; import 'package:star_lock/talk/startChart/entity/scp_message.dart'; @@ -26,6 +28,7 @@ import 'package:star_lock/talk/startChart/handle/scp_message_handle.dart'; import 'package:star_lock/talk/startChart/handle/scp_message_handler_factory.dart'; import 'package:star_lock/talk/startChart/proto/talk_data.pb.dart'; import 'package:star_lock/talk/startChart/proto/talk_expect.pb.dart'; +import 'package:star_lock/talk/startChart/proto/talk_expect.pbserver.dart'; import 'package:star_lock/talk/startChart/start_chart_talk_status.dart'; import 'package:star_lock/tools/baseGetXController.dart'; import 'package:star_lock/tools/deviceInfo_utils.dart'; @@ -83,9 +86,9 @@ class StartChartManage { final int _maxPayloadSize = 8 * 1024; // 分包大小 // 默认通话的期望数据格式 - TalkExpect defaultTalkExpect = TalkExpect( - videoType: [TalkExpect_VideoTypeE.IMAGE], - audioType: [TalkExpect_AudioTypeE.G711], + TalkExpectReq defaultTalkExpect = TalkExpectReq( + videoType: [VideoTypeE.IMAGE], + audioType: [AudioTypeE.G711], ); // 默认通话数据 @@ -345,7 +348,9 @@ class StartChartManage { MessageId: MessageCommand.getNextMessageId(ToPeerId, increment: true), ); await _sendMessage(message: message); + // 设置状态为等待接听 talkStatus.setWaitingAnswer(); + _log(text: '发送同意接听消息'); } // 发送拒绝接听消息 @@ -360,10 +365,15 @@ class StartChartManage { MessageId: MessageCommand.getNextMessageId(ToPeerId, increment: true), ); await _sendMessage(message: message); + + // 设置状态为拒绝 + StartChartTalkStatus.instance.setRejected(); + // 停止播放铃声 + AudioPlayerManager().stopRingtone(); } // 发送期望接受消息 - void sendTalkExpectMessage({required TalkExpect talkExpect}) async { + void sendTalkExpectMessage({required TalkExpectReq talkExpect}) async { final message = MessageCommand.talkExpectMessage( ToPeerId: ToPeerId, FromPeerId: FromPeerId, @@ -424,6 +434,11 @@ class StartChartManage { MessageId: MessageCommand.getNextMessageId(ToPeerId, increment: true), ); await _sendMessage(message: message); + + // 设置状态为通话中挂断 + StartChartTalkStatus.instance.setHangingUpDuring(); + // 停止播放铃声 + AudioPlayerManager().stopRingtone(); } // 重新上线 @@ -785,8 +800,7 @@ class StartChartManage { } } } catch (e, stackTrace) { - throw StartChartMessageException( - '❌ Udp result data error ----> $e\n,$stackTrace'); + throw StartChartMessageException('$e\n,$stackTrace'); } } }); @@ -958,7 +972,7 @@ class StartChartManage { } /// 修改预期接收到的数据 - void changeTalkExpectDataType({required TalkExpect talkExpect}) { + void changeTalkExpectDataType({required TalkExpectReq talkExpect}) { defaultTalkExpect = talkExpect; } diff --git a/lib/talk/startChart/start_chart_talk_status.dart b/lib/talk/startChart/start_chart_talk_status.dart index 6c8b5f11..7f4a7f2a 100644 --- a/lib/talk/startChart/start_chart_talk_status.dart +++ b/lib/talk/startChart/start_chart_talk_status.dart @@ -1,131 +1,70 @@ -// 定义视频对讲的状态枚举 +import 'dart:async'; import 'package:star_lock/blue/io_tool/manager_event_bus.dart'; -import 'package:star_lock/talk/startChart/events/talk_status_change_event.dart'; - -enum TalkStatus { - waitingAnswer, // 等待接听 - answeredSuccessfully, // 接听成功 - waitingData, // 等待数据 - duringCall, // 通话中 - hangingUpDuring, // 通话中挂断 - rejected, // 被拒绝 - uninitialized, // 未初始化 - initializationCompleted, // 初始化完成 - notTalkData, // 暂无通话数据 - notTalkPing, // 暂无通话保持 - error, // 错误状态 - end, // 结束 -} +import 'package:star_lock/talk/startChart/constant/talk_status.dart'; class StartChartTalkStatus { // 私有字段,用于存储当前状态 TalkStatus _status = TalkStatus.uninitialized; // 私有化默认构造函数,防止外部创建实例 - StartChartTalkStatus._( - {TalkStatus initialStatus = TalkStatus.uninitialized}) { - _status = initialStatus; - } + StartChartTalkStatus._(); // 静态私有字段,存储唯一的实例 static final StartChartTalkStatus _instance = StartChartTalkStatus._(); - // 静态工厂构造函数,返回唯一的实例 + // 提供一个静态方法来获取单例实例 static StartChartTalkStatus get instance => _instance; + // 创建一个 StreamController 用于状态变化 + final StreamController _statusStreamController = + StreamController.broadcast(); + + // 提供一个方法来获取 Stream + Stream get statusStream => _statusStreamController.stream; + // 获取当前状态的 getter 方法 TalkStatus get status => _status; - // 通用的 set 方法(仍然保留) - set status(TalkStatus newStatus) { - _setStatus(newStatus); - } - // 内部方法,用于更新状态并触发状态变化事件 void _setStatus(TalkStatus newStatus) { if (_status == newStatus) return; // 如果状态没有变化,直接返回 - print("对讲状态变化: ${_status.name} -> ${newStatus.name}"); + print("对讲状态变化: ${_status} -> ${newStatus}"); + // 更新状态 _status = newStatus; - // 触发状态变化的事件或执行其他操作 - _onStatusChanged(newStatus); + // 触发状态变化事件 + _statusStreamController.add(_status); } - // 状态变化时的回调方法(可选) - void _onStatusChanged(TalkStatus newStatus) { - // 发布状态变化事件 - EventBusManager().eventBus!.fire(TalkStatusChangeEvent(_status, newStatus)); - } + // 提供状态设置方法 + void setWaitingAnswer() => _setStatus(TalkStatus.waitingAnswer); - /// 设置状态为等待接听 - void setWaitingAnswer() { - _setStatus(TalkStatus.waitingAnswer); - // 可以在这里添加特定于 "waitingAnswer" 状态的逻辑 - } + void setWaitingData() => _setStatus(TalkStatus.waitingData); - /// 设置状态为等待数据 - void setWaitingData() { - _setStatus(TalkStatus.waitingData); - // 可以在这里添加特定于 "waitingAnswer" 状态的逻辑 - } + void setDuringCall() => _setStatus(TalkStatus.duringCall); + void setRejected() => _setStatus(TalkStatus.rejected); - /// 设置状态为通话中 - void setDuringCall() { - _setStatus(TalkStatus.duringCall); - // 可以在这里添加特定于 "duringCall" 状态的逻辑 - } + void setUninitialized() => _setStatus(TalkStatus.uninitialized); - /// 设置状态为被拒绝 - void setRejected() { - _setStatus(TalkStatus.rejected); - // 可以在这里添加特定于 "rejected" 状态的逻辑 - } + void setInitializationCompleted() => + _setStatus(TalkStatus.initializationCompleted); - /// 设置状态为未初始化 - void setUninitialized() { - _setStatus(TalkStatus.uninitialized); - // 可以在这里添加特定于 "uninitialized" 状态的逻辑 - } + void setNotTalkData() => _setStatus(TalkStatus.notTalkData); - /// 设置状态为初始化完成 - void setInitializationCompleted() { - _setStatus(TalkStatus.initializationCompleted); - // 可以为 "initializationCompleted" 状态添加特定的逻辑 - } + void setNotTalkPing() => _setStatus(TalkStatus.notTalkPing); - /// 设置状态为暂无通话数据 - void setNotTalkData() { - _setStatus(TalkStatus.notTalkData); - // 可以在这里添加特定于 "notTalkData" 状态的逻辑 - } + void setError() => _setStatus(TalkStatus.error); - /// 设置状态为暂无通话保持 - void setNotTalkPing() { - _setStatus(TalkStatus.notTalkPing); - // 可以在这里添加特定于 "notTalkPing" 状态的逻辑 - } + void setHangingUpDuring() => _setStatus(TalkStatus.hangingUpDuring); - /// 设置状态为错误 - void setError() { - _setStatus(TalkStatus.error); - // 可以在这里添加特定于 "error" 状态的逻辑 - } + void setAnsweredSuccessfully() => _setStatus(TalkStatus.answeredSuccessfully); - /// 设置状态为通话中挂断 - void setHangingUpDuring() { - _setStatus(TalkStatus.hangingUpDuring); - // 可以在这里添加特定于 "hangingUpDuring" 状态的逻辑 - } /// 设置状态为接听成功 - void setAnsweredSuccessfully() { - _setStatus(TalkStatus.answeredSuccessfully); - // 可以在这里添加特定于 "hangingUpDuring" 状态的逻辑 - } + void setEnd() => _setStatus(TalkStatus.end); - /// 设置状态为结束 - void setEnd() { - _setStatus(TalkStatus.end); - // 可以在这里添加特定于 "end" 状态的逻辑 + // 提供一个方法来关闭 StreamController + void dispose() { + _statusStreamController.close(); } } diff --git a/lib/talk/startChart/views/talkView/talk_view_logic.dart b/lib/talk/startChart/views/talkView/talk_view_logic.dart new file mode 100644 index 00000000..264374bb --- /dev/null +++ b/lib/talk/startChart/views/talkView/talk_view_logic.dart @@ -0,0 +1,291 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:math'; + +import 'package:flutter/services.dart'; +import 'package:flutter_pcm_sound/flutter_pcm_sound.dart'; +import 'package:flutter_voice_processor/flutter_voice_processor.dart'; +import 'package:get/get.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:star_lock/app_settings/app_settings.dart'; +import 'package:star_lock/blue/io_tool/manager_event_bus.dart'; +import 'package:star_lock/talk/call/callTalk.dart'; +import 'package:star_lock/talk/startChart/constant/talk_status.dart'; +import 'package:star_lock/talk/startChart/proto/talk_data.pb.dart'; +import 'package:star_lock/talk/startChart/proto/talk_data.pbenum.dart'; +import 'package:star_lock/talk/startChart/start_chart_manage.dart'; +import 'package:star_lock/talk/startChart/start_chart_talk_status.dart'; +import 'package:star_lock/talk/startChart/views/talkView/talk_view_state.dart'; + +import '../../../../talk/call/g711.dart'; +import '../../../../talk/udp/udp_manage.dart'; +import '../../../../talk/udp/udp_senderManage.dart'; +import '../../../../tools/baseGetXController.dart'; +import '../../../../tools/eventBusEventManage.dart'; + +class TalkViewLogic extends BaseGetXController { + final TalkViewState state = TalkViewState(); + + Timer? _syncTimer; + int _startTime = 0; + final int bufferSize = 22; // 缓冲区大小(以帧为单位) + final List frameTimestamps = []; + int frameIntervalMs = 45; // 初始帧间隔设置为45毫秒(约22FPS) + int minFrameIntervalMs = 30; // 最小帧间隔(约33 FPS) + int maxFrameIntervalMs = 100; // 最大帧间隔(约10 FPS) + + /// 收到Talk发送的状态 + StreamSubscription? _getTalkStatusRefreshUIEvent; + + void _initFlutterPcmSound() { + const int sampleRate = 44100; + FlutterPcmSound.setLogLevel(LogLevel.verbose); + FlutterPcmSound.setup(sampleRate: sampleRate, channelCount: 2); + // 设置 feed 阈值 + if (Platform.isAndroid) { + FlutterPcmSound.setFeedThreshold(-1); // Android 平台的特殊处理 + } else { + FlutterPcmSound.setFeedThreshold(sampleRate ~/ 32); // 非 Android 平台的处理 + } + } + + /// 挂断 + void udpHangUpAction() async { + if (state.talkStatus.value == TalkStatus.duringCall) { + // 如果是通话中就挂断 + StartChartManage().sendTalkHangupMessage(); + } else { + // 拒绝 + StartChartManage().sendTalkRejectMessage(); + } + Get.back(); + } + + // 发起接听命令 + void initiateAnswerCommand() { + StartChartManage().sendTalkAcceptMessage(); + } + + void _updateFps(List frameTimestamps) { + final int now = DateTime.now().millisecondsSinceEpoch; + // 移除超过1秒的时间戳 + frameTimestamps.removeWhere((timestamp) => now - timestamp > 1000); + + // 计算 FPS + final double fps = frameTimestamps.length.toDouble(); + + // 更新 FPS + state.fps.value = fps; + } + + // 监听音视频数据流 + void _startListenTalkData() { + state.talkDataRepository.talkDataStream.listen((talkData) { + final contentType = talkData.contentType; + // 判断数据类型,进行分发处理 + switch (contentType) { + case TalkData_ContentTypeE.G711: + // state.audioBuffer.add(talkData); + if (state.audioBuffer.length < 60) { + // 假设缓冲区大小为60帧 + state.audioBuffer.add(talkData); + } + break; + case TalkData_ContentTypeE.Image: + // state.videoBuffer.add(talkData); + // 增加视频缓冲区大小 + if (state.videoBuffer.length < 60) { + // 假设缓冲区大小为60帧 + state.videoBuffer.add(talkData); + } + break; + } + }); + } + + /// 监听对讲状态 + void _startListenTalkStatus() { + state.startChartTalkStatus.statusStream.listen((talkStatus) { + state.talkStatus.value = talkStatus; + switch (talkStatus) { + case TalkStatus.rejected: + case TalkStatus.hangingUpDuring: + case TalkStatus.notTalkData: + case TalkStatus.notTalkPing: + case TalkStatus.end: + _handleInvalidTalkStatus(); + break; + default: + // 其他状态的处理 + break; + } + }); + } + + void _playAudioData(TalkData talkData) { + // 将 PCM 数据转换为 PcmArrayInt16 + final PcmArrayInt16 fromList = PcmArrayInt16.fromList(talkData.content); + FlutterPcmSound.feed(fromList); + if (!state.isPlaying.value) { + FlutterPcmSound.play(); + state.isPlaying.value = true; + } + } + + void _playVideoData(TalkData talkData) { + state.listData.value = Uint8List.fromList(talkData.content); + } + + void _startPlayback() { + int frameIntervalMs = 45; // 初始帧间隔设置为45毫秒(约22FPS) + + Future.delayed(Duration(milliseconds: 800), () { + _startTime = DateTime.now().millisecondsSinceEpoch; + _syncTimer ??= + Timer.periodic(Duration(milliseconds: frameIntervalMs), (timer) { + final currentTime = DateTime.now().millisecondsSinceEpoch; + final elapsedTime = currentTime - _startTime; + + // 根据 elapsedTime 同步音频和视频 + // AppLog.log('Elapsed Time: $elapsedTime ms'); + // 动态调整帧间隔 + _adjustFrameInterval(); + + // 播放合适的音频帧 + if (state.audioBuffer.isNotEmpty && + state.audioBuffer.first.durationMs <= elapsedTime) { + _playAudioData(state.audioBuffer.removeAt(0)); + } + + // 播放合适的视频帧 + // 跳帧策略:如果缓冲区中有多个帧,且它们的时间戳都在当前时间之前,则播放最新的帧 + while (state.videoBuffer.isNotEmpty && + state.videoBuffer.first.durationMs <= elapsedTime) { + // 如果有多个帧,移除旧的帧,保持最新的帧 + if (state.videoBuffer.length > 1) { + state.videoBuffer.removeAt(0); + } else { + // 记录当前时间戳 + frameTimestamps.add(DateTime.now().millisecondsSinceEpoch); + // 计算并更新 FPS + _updateFps(frameTimestamps); + _playVideoData(state.videoBuffer.removeAt(0)); + } + } + }); + }); + } + + /// 动态调整帧间隔 + void _adjustFrameInterval() { + if (state.videoBuffer.length < 10 && frameIntervalMs < maxFrameIntervalMs) { + // 如果缓冲区较小且帧间隔小于最大值,则增加帧间隔 + frameIntervalMs += 5; + } else if (state.videoBuffer.length > 20 && + frameIntervalMs > minFrameIntervalMs) { + // 如果缓冲区较大且帧间隔大于最小值,则减少帧间隔 + frameIntervalMs -= 5; + } + _syncTimer?.cancel(); + _syncTimer = + Timer.periodic(Duration(milliseconds: frameIntervalMs), (timer) { + final currentTime = DateTime.now().millisecondsSinceEpoch; + final elapsedTime = currentTime - _startTime; + + // 播放合适的音频帧 + if (state.audioBuffer.isNotEmpty && + state.audioBuffer.first.durationMs <= elapsedTime) { + _playAudioData(state.audioBuffer.removeAt(0)); + } + + // 播放合适的视频帧 + // 跳帧策略:如果缓冲区中有多个帧,且它们的时间戳都在当前时间之前,则播放最新的帧 + while (state.videoBuffer.isNotEmpty && + state.videoBuffer.first.durationMs <= elapsedTime) { + // 如果有多个帧,移除旧的帧,保持最新的帧 + if (state.videoBuffer.length > 1) { + state.videoBuffer.removeAt(0); + } else { + // 记录当前时间戳 + frameTimestamps.add(DateTime.now().millisecondsSinceEpoch); + // 计算并更新 FPS + _updateFps(frameTimestamps); + _playVideoData(state.videoBuffer.removeAt(0)); + } + } + }); + } + + /// 停止播放音频 + void _stopPlayG711Data() async { + print('停止播放'); + await FlutterPcmSound.pause(); + await FlutterPcmSound.stop(); + await FlutterPcmSound.clear(); + } + + /// 开门 + udpOpenDoorAction(List list) async {} + + Future getPermissionStatus() async { + final Permission permission = Permission.microphone; + //granted 通过,denied 被拒绝,permanentlyDenied 拒绝且不在提示 + final 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; + } + + ///申请权限 + void requestPermission(Permission permission) async { + final PermissionStatus status = await permission.request(); + if (status.isPermanentlyDenied) { + openAppSettings(); + } + } + + @override + void onReady() { + super.onReady(); + } + + @override + void onInit() { + super.onInit(); + + // 监听音视频数据流 + _startListenTalkData(); + // 监听对讲状态 + _startListenTalkStatus(); + // 在没有监听成功之前赋值一遍状态 + // *** 由于页面会在状态变化之后才会初始化,导致识别不到最新的状态,在这里手动赋值 *** + state.talkStatus.value = state.startChartTalkStatus.status; + + _initFlutterPcmSound(); + + _startPlayback(); + } + + @override + void onClose() { + _stopPlayG711Data(); + state.listData.value = Uint8List(0); + _syncTimer?.cancel(); + } + + /// 处理无效通话状态 + void _handleInvalidTalkStatus() { + state.listData.value = Uint8List(0); + // 停止播放音频 + _stopPlayG711Data(); + // 状态错误,返回页面 + Get.back(); + } +} diff --git a/lib/talk/startChart/views/talkView/talk_view_page.dart b/lib/talk/startChart/views/talkView/talk_view_page.dart new file mode 100644 index 00000000..db49b6ba --- /dev/null +++ b/lib/talk/startChart/views/talkView/talk_view_page.dart @@ -0,0 +1,373 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:star_lock/app_settings/app_settings.dart'; +import 'package:star_lock/main/lockDetail/realTimePicture/realTimePicture_state.dart'; +import 'package:star_lock/talk/call/callTalk.dart'; +import 'package:star_lock/talk/startChart/constant/talk_status.dart'; +import 'package:star_lock/talk/startChart/start_chart_talk_status.dart'; +import 'package:star_lock/talk/startChart/views/talkView/talk_view_logic.dart'; +import 'package:star_lock/talk/startChart/views/talkView/talk_view_state.dart'; + +import '../../../../app_settings/app_colors.dart'; +import '../../../../tools/showTFView.dart'; + +class TalkViewPage extends StatefulWidget { + const TalkViewPage({Key? key}) : super(key: key); + + @override + State createState() => _TalkViewPageState(); +} + +class _TalkViewPageState extends State + with TickerProviderStateMixin { + final TalkViewLogic logic = Get.put(TalkViewLogic()); + final TalkViewState state = Get.find().state; + + @override + void initState() { + super.initState(); + + //写一个定时器,三十秒后页面自动返回 + // state.autoBackTimer = Timer(const Duration(seconds: 30), Get.back); + + state.animationController = AnimationController( + vsync: this, // 确保使用的TickerProvider是当前Widget + duration: const Duration(seconds: 1), + ); + + state.animationController.repeat(); + //动画开始、结束、向前移动或向后移动时会调用StatusListener + state.animationController.addStatusListener((AnimationStatus status) { + // AppLog.log("AnimationStatus:$status"); + if (status == AnimationStatus.completed) { + state.animationController.reset(); + state.animationController.forward(); + } else if (status == AnimationStatus.dismissed) { + state.animationController.reset(); + state.animationController.forward(); + } + }); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 1.sw, + height: 1.sh, + child: Stack( + alignment: Alignment.center, + children: [ + Obx( + () => state.listData.value.isEmpty + ? Image.asset( + 'images/main/monitorBg.png', + width: ScreenUtil().screenWidth, + height: ScreenUtil().screenHeight, + fit: BoxFit.cover, + ) + : Image.memory( + state.listData.value, + gaplessPlayback: true, + width: 1.sw, + height: 1.sh, + fit: BoxFit.cover, + filterQuality: FilterQuality.high, + errorBuilder: ( + BuildContext context, + Object error, + StackTrace? stackTrace, + ) { + return Container(color: Colors.transparent); + }, + ), + ), + Obx(() => state.listData.value.isEmpty + ? Positioned( + bottom: 300.h, + child: Text( + '正在创建安全连接...'.tr, + style: TextStyle(color: Colors.black, fontSize: 26.sp), + )) + : Container()), + Positioned( + bottom: 10.w, + child: Container( + width: 1.sw - 30.w * 2, + // height: 300.h, + margin: EdgeInsets.all(30.w), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.2), + borderRadius: BorderRadius.circular(20.h)), + child: Column( + children: [ + SizedBox(height: 20.h), + bottomTopBtnWidget(), + SizedBox(height: 20.h), + bottomBottomBtnWidget(), + SizedBox(height: 20.h), + ], + ), + ), + ), + Positioned( + top: 100.h, + left: 10.w, + child: Obx( + () => Text( + 'FPS:${state.fps.value}', + style: TextStyle(fontSize: 30.sp, color: Colors.orange,fontWeight: FontWeight.bold), + ), + ), + ), + Obx(() => state.listData.value.isEmpty + ? buildRotationTransition() + : Container()) + ], + ), + ); + } + + Widget bottomTopBtnWidget() { + return Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + // 打开关闭声音 + GestureDetector( + onTap: () { + state.isOpenVoice.value = !state.isOpenVoice.value; + }, + child: Container( + width: 50.w, + height: 50.w, + padding: EdgeInsets.all(5.w), + child: Obx(() => Image( + width: 40.w, + height: 40.w, + image: state.isOpenVoice.value + ? const AssetImage( + 'images/main/icon_lockDetail_monitoringCloseVoice.png') + : const AssetImage( + 'images/main/icon_lockDetail_monitoringOpenVoice.png'))), + ), + ), + SizedBox(width: 50.w), + // 截图 + GestureDetector( + onTap: () { + // Get.toNamed(Routers.monitoringRealTimeScreenPage); + }, + child: Container( + width: 50.w, + height: 50.w, + padding: EdgeInsets.all(5.w), + child: Image( + width: 40.w, + height: 40.w, + image: const AssetImage( + 'images/main/icon_lockDetail_monitoringScreenshot.png')), + ), + ), + SizedBox(width: 50.w), + // 录制 + GestureDetector( + onTap: () { + // Get.toNamed(Routers.monitoringRealTimeScreenPage); + }, + child: Container( + width: 50.w, + height: 50.w, + padding: EdgeInsets.all(5.w), + child: Image( + width: 40.w, + height: 40.w, + fit: BoxFit.fill, + image: const AssetImage( + 'images/main/icon_lockDetail_monitoringScreenRecording.png')), + ), + ), + SizedBox(width: 50.w), + GestureDetector( + onTap: () { + logic.showToast('功能暂未开放'.tr); + }, + child: Image( + width: 28.w, + height: 28.w, + fit: BoxFit.fill, + image: const AssetImage( + 'images/main/icon_lockDetail_rectangle.png'))) + ]); + } + + Widget bottomBottomBtnWidget() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // 接听 + Obx( + () => bottomBtnItemWidget( + getAnswerBtnImg(), + getAnswerBtnName(), + Colors.white, + longPress: () async {}, + longPressUp: () async {}, + onClick: () async { + if (state.talkStatus.value == TalkStatus.waitingAnswer) { + // 接听 + logic.initiateAnswerCommand(); + } + }, + ), + ), + bottomBtnItemWidget( + 'images/main/icon_lockDetail_hangUp.png', '挂断'.tr, Colors.red, + onClick: () { + // 挂断 + logic.udpHangUpAction(); + }), + bottomBtnItemWidget( + 'images/main/icon_lockDetail_monitoringUnlock.png', + '开锁', + AppColors.mainColor, + onClick: () {}, + ) + ]); + } + + String getAnswerBtnImg() { + switch (state.talkStatus.value) { + case TalkStatus.waitingAnswer: + return 'images/main/icon_lockDetail_monitoringAnswerCalls.png'; + case TalkStatus.answeredSuccessfully: + case TalkStatus.duringCall: + return 'images/main/icon_lockDetail_monitoringUnTalkback.png'; + default: + return 'images/main/icon_lockDetail_monitoringAnswerCalls.png'; + } + } + + String getAnswerBtnName() { + switch (state.talkStatus.value) { + case TalkStatus.waitingAnswer: + return '接听'.tr; + case TalkStatus.answeredSuccessfully: + case TalkStatus.duringCall: + return '长按说话'.tr; + default: + return '接听'.tr; + } + } + + Widget bottomBtnItemWidget(String iconUrl, String name, Color backgroundColor, + {required Function() onClick, + Function()? longPress, + Function()? longPressUp}) { + double wh = 80.w; + return GestureDetector( + onTap: onClick, + onLongPress: longPress, + onLongPressUp: longPressUp, + child: SizedBox( + height: 140.h, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: wh, + height: wh, + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular((wh + 10.w * 2) / 2)), + padding: EdgeInsets.all(20.w), + child: Image.asset(iconUrl, fit: BoxFit.fitWidth), + ), + SizedBox(height: 20.w), + Expanded( + child: Text(name, + style: TextStyle(fontSize: 20.sp, color: Colors.white), + textAlign: TextAlign.center)) + ], + )), + ); + } + + void showDeletPasswordAlertDialog(BuildContext context) { + showDialog( + barrierDismissible: false, + context: context, + builder: (BuildContext context) { + return ShowTFView( + title: '请输入六位数字开锁密码'.tr, + tipTitle: '', + controller: state.passwordTF, + inputFormatters: [ + LengthLimitingTextInputFormatter(6), //限制长度 + FilteringTextInputFormatter.allow(RegExp('[0-9]')), + ], + sureClick: () async { + // //发送删除锁请求 + // if (state.passwordTF.text.isEmpty) { + // logic.showToast('请输入开锁密码'.tr); + // return; + // } + // + // // List numbers = state.passwordTF.text.split('').map((char) => int.parse(char)).toList(); + // // 开锁 + // // lockID + // List numbers = []; + // List lockIDData = utf8.encode(state.passwordTF.text); + // numbers.addAll(lockIDData); + // // topBytes = getFixedLengthList(lockIDData, 20 - lockIDData.length); + // for (int i = 0; i < 6 - lockIDData.length; i++) { + // numbers.add(0); + // } + }, + cancelClick: () { + Get.back(); + }, + ); + }, + ); + } + + //旋转动画 + Widget buildRotationTransition() { + return Positioned( + left: ScreenUtil().screenWidth / 2 - 220.w / 2, + top: ScreenUtil().screenHeight / 2 - 220.w / 2 - 150.h, + child: GestureDetector( + child: RotationTransition( + //设置动画的旋转中心 + alignment: Alignment.center, + //动画控制器 + turns: state.animationController, + //将要执行动画的子view + child: AnimatedOpacity( + opacity: 0.5, + duration: const Duration(seconds: 2), + child: Image.asset( + 'images/main/realTime_connecting.png', + width: 220.w, + height: 220.w, + ), + ), + ), + onTap: () { + state.animationController.forward(); + }, + ), + ); + } + + @override + void dispose() { + state.animationController.dispose(); + state.realTimePicTimer.cancel(); + state.autoBackTimer.cancel(); + CallTalk().finishAVData(); + super.dispose(); + } +} diff --git a/lib/talk/startChart/views/talkView/talk_view_state.dart b/lib/talk/startChart/views/talkView/talk_view_state.dart new file mode 100644 index 00000000..403ede77 --- /dev/null +++ b/lib/talk/startChart/views/talkView/talk_view_state.dart @@ -0,0 +1,68 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_voice_processor/flutter_voice_processor.dart'; +import 'package:get/get.dart'; +import 'package:get/get_rx/get_rx.dart'; +import 'package:network_info_plus/network_info_plus.dart'; +import 'package:star_lock/talk/startChart/constant/talk_status.dart'; +import 'package:star_lock/talk/startChart/handle/other/talk_data_repository.dart'; +import 'package:star_lock/talk/startChart/proto/talk_data.pb.dart'; +import 'package:star_lock/talk/startChart/start_chart_talk_status.dart'; + +import '../../../../tools/storage.dart'; + +class TalkViewState { + RxBool isOpenVoice = false.obs; + int udpSendDataFrameNumber = 0; // 帧序号 + // var isSenderAudioData = false.obs;// 是否要发送音频数据 + + Future userMobileIP = NetworkInfo().getWifiIP(); + Future userUid = Storage.getUid(); + + RxInt udpStatus = + 0.obs; //0:初始状态 1:等待监视 2: 3:监视中 4:呼叫成功 5:主角通话中 6:被叫通话 8:被叫通话中 9:长按说话 + TextEditingController passwordTF = TextEditingController(); + + Rx listData = Uint8List(0).obs; //得到的视频流字节数据 + RxList listAudioData = [].obs; //得到的音频流字节数据 + + late final VoiceProcessor? voiceProcessor; + + late Timer oneMinuteTimeTimer = + Timer(const Duration(seconds: 1), () {}); // 定时器超过60秒关闭当前界面 + RxInt oneMinuteTime = 0.obs; // 定时器秒数 + + // 定时器如果发送了接听的命令 而没收到回复就每秒重复发送10次 + late Timer answerTimer; + late Timer hangUpTimer; + late Timer openDoorTimer; + late AnimationController animationController; + + RxDouble fps = 0.0.obs; // 添加 FPS 计数 + + + late Timer autoBackTimer = + Timer(const Duration(seconds: 1), () {}); //发送30秒监视后自动返回 + late Timer realTimePicTimer = + Timer(const Duration(seconds: 1), () {}); //监视命令定时器 + RxInt elapsedSeconds = 0.obs; + + + List audioBuffer = [].obs; + List videoBuffer = [].obs; + + + RxBool isPlaying = false.obs; // 是否开始播放 + + + Rx talkStatus = TalkStatus.none.obs; //星图对讲状态 + + // 获取 startChartTalkStatus 的唯一实例 + final StartChartTalkStatus startChartTalkStatus = + StartChartTalkStatus.instance; + + // 通话数据流的单例流数据处理类 + final TalkDataRepository talkDataRepository = TalkDataRepository.instance; +}