From 8478dd33d1456eedfb930f4df8f233c7929f7459 Mon Sep 17 00:00:00 2001 From: liyi Date: Tue, 22 Apr 2025 15:17:42 +0800 Subject: [PATCH 001/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E4=BC=98?= =?UTF-8?q?=E5=8C=96h264=E6=92=AD=E6=94=BE=E9=80=BB=E8=BE=91=E5=B9=B6?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=9F=B3=E9=A2=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lockDetail/lockDetail_logic.dart | 3 +- .../lockMian/entity/lockListInfo_entity.dart | 12 ++ .../handle/impl/udp_echo_test_handler.dart | 3 +- .../handle/impl/udp_talk_accept_handler.dart | 23 ++- .../handle/impl/udp_talk_data_handler.dart | 17 ++- .../handle/impl/udp_talk_expect_handler.dart | 4 +- .../handle/impl/udp_talk_request_handler.dart | 37 +++-- .../handle/other/h264_frame_handler.dart | 11 +- .../handle/other/talk_data_model.dart | 9 ++ .../handle/other/talk_data_repository.dart | 9 +- .../handle/scp_message_base_handle.dart | 6 +- lib/talk/starChart/star_chart_manage.dart | 37 ++--- .../views/talkView/talk_view_logic.dart | 27 +--- .../starChart/webView/h264_web_logic.dart | 133 +++++++++++++++--- .../webView/h264_web_view_state.dart | 3 + 15 files changed, 244 insertions(+), 90 deletions(-) create mode 100644 lib/talk/starChart/handle/other/talk_data_model.dart diff --git a/lib/main/lockDetail/lockDetail/lockDetail_logic.dart b/lib/main/lockDetail/lockDetail/lockDetail_logic.dart index f3668a2c..c03a2753 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_logic.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_logic.dart @@ -774,9 +774,10 @@ class LockDetailLogic extends BaseGetXController { showToast('设备未配网'.tr); return; } + // 发送监控id StartChartManage().startCallRequestMessageTimer( - ToPeerId: StartChartManage().lockPeerId ?? ''); + ToPeerId: StartChartManage().lockNetworkInfo.peerId ?? ''); } else { showToast('猫眼设置为省电模式时无法进行监控,请在猫眼设置中切换为其他模式'.tr); } diff --git a/lib/main/lockMian/entity/lockListInfo_entity.dart b/lib/main/lockMian/entity/lockListInfo_entity.dart index 754891c1..b98bb328 100755 --- a/lib/main/lockMian/entity/lockListInfo_entity.dart +++ b/lib/main/lockMian/entity/lockListInfo_entity.dart @@ -374,6 +374,9 @@ class LockFeature { this.isNoSupportedBlueBroadcast, this.wifiLockType, this.wifi, + this.isH264, + this.isH265, + this.isMJpeg, }); LockFeature.fromJson(Map json) { @@ -391,6 +394,9 @@ class LockFeature { isNoSupportedBlueBroadcast = json['isNoSupportedBlueBroadcast']; wifiLockType = json['wifiLockType']; wifi = json['wifi']; + isH264 = json['isH264']; + isH265 = json['isH265']; + isMJpeg = json['isMJpeg']; } int? password; @@ -407,6 +413,9 @@ class LockFeature { int? isNoSupportedBlueBroadcast; int? wifiLockType; int? wifi; + int? isH264; + int? isH265; + int? isMJpeg; Map toJson() { final Map data = {}; @@ -424,6 +433,9 @@ class LockFeature { data['isNoSupportedBlueBroadcast'] = isNoSupportedBlueBroadcast; data['wifiLockType'] = wifiLockType; data['wifi'] = wifi; + data['isH264'] = isH264; + data['isH265'] = isH265; + data['isMJpeg'] = isMJpeg; return data; } } diff --git a/lib/talk/starChart/handle/impl/udp_echo_test_handler.dart b/lib/talk/starChart/handle/impl/udp_echo_test_handler.dart index beebeb75..cff8154f 100644 --- a/lib/talk/starChart/handle/impl/udp_echo_test_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_echo_test_handler.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:get/get.dart'; import 'package:star_lock/talk/starChart/entity/scp_message.dart'; +import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart'; import 'package:star_lock/talk/starChart/handle/scp_message_base_handle.dart'; import 'package:star_lock/talk/starChart/handle/scp_message_handle.dart'; import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart'; @@ -22,7 +23,7 @@ class UdpEchoTestHandler extends ScpMessageBaseHandle EasyLoading.showToast(scpMessage.Payload, duration: 2000.milliseconds); } else { talkDataRepository.addTalkData( - TalkData(content: payload, contentType: TalkData_ContentTypeE.Image)); + TalkDataModel(talkData: TalkData(content: payload, contentType: TalkData_ContentTypeE.Image))); } } diff --git a/lib/talk/starChart/handle/impl/udp_talk_accept_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_accept_handler.dart index 5f71aafa..2605525c 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_accept_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_accept_handler.dart @@ -4,6 +4,7 @@ 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/main/lockMian/entity/lockListInfo_entity.dart'; import 'package:star_lock/talk/starChart/constant/message_type_constant.dart'; import 'package:star_lock/talk/starChart/constant/talk_status.dart'; import 'package:star_lock/talk/starChart/entity/scp_message.dart'; @@ -13,6 +14,7 @@ import 'package:star_lock/talk/starChart/proto/gateway_reset.pb.dart'; import 'package:star_lock/talk/starChart/proto/generic.pb.dart'; import 'package:star_lock/talk/starChart/proto/talk_accept.pb.dart'; import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart'; +import 'package:star_lock/tools/commonDataManage.dart'; import '../../star_chart_manage.dart'; @@ -77,7 +79,24 @@ class UdpTalkAcceptHandler extends ScpMessageBaseHandle } void _handleSendExpect() { - // 修改预期数据并启动发送预期数据定时器,在收到回复时停止 - startChartManage.sendImageVideoAndG711AudioTalkExpectData(); + final LockListInfoItemEntity currentKeyInfo = + CommonDataManage().currentKeyInfo; + final isH264 = currentKeyInfo.lockFeature?.isH264 == 1; + final isMJpeg = currentKeyInfo.lockFeature?.isMJpeg == 1; + + // 优先使用H264,其次是MJPEG + if (isH264) { + // 锁支持H264,发送H264视频和G711音频期望 + startChartManage.sendH264VideoAndG711AudioTalkExpectData(); + print('锁支持H264,发送H264视频格式期望数据'); + } else if (isMJpeg) { + // 锁只支持MJPEG,发送图像视频和G711音频期望 + startChartManage.sendImageVideoAndG711AudioTalkExpectData(); + print('锁不支持H264,支持MJPEG,发送MJPEG视频格式期望数据'); + } else { + // 默认使用图像视频 + startChartManage.sendImageVideoAndG711AudioTalkExpectData(); + print('锁不支持H264和MJPEG,默认发送图像视频格式期望数据'); + } } } diff --git a/lib/talk/starChart/handle/impl/udp_talk_data_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_data_handler.dart index 0a9e7ab3..44705624 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_data_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_data_handler.dart @@ -5,6 +5,7 @@ import 'package:star_lock/talk/starChart/constant/message_type_constant.dart'; import 'package:star_lock/talk/starChart/entity/scp_message.dart'; import 'package:star_lock/talk/starChart/handle/other/h264_frame_handler.dart'; import 'package:star_lock/talk/starChart/handle/other/packet_loss_statistics.dart'; +import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart'; import 'package:star_lock/talk/starChart/handle/scp_message_base_handle.dart'; import 'package:star_lock/talk/starChart/handle/scp_message_handle.dart'; import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart'; @@ -63,7 +64,7 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle int? messageId}) { // 获取统计信息 final stats = PacketLossStatistics().getStatistics(); - _asyncLog('丢包统计: $stats'); + // _asyncLog('丢包统计: $stats'); // _asyncLog( // '分包数据:messageId:$messageId [$spIndex/$spTotal] PayloadLength:$PayloadLength'); if (messageType == MessageTypeConstant.RealTimeData) { @@ -118,7 +119,7 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle void _handleVideoH264(TalkData talkData) { final TalkDataH264Frame talkDataH264Frame = TalkDataH264Frame(); talkDataH264Frame.mergeFromBuffer(talkData.content); - frameHandler.handleFrame(talkDataH264Frame); + frameHandler.handleFrame(talkDataH264Frame, talkData); } /// 处理图片数据 @@ -127,7 +128,11 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle await _processCompletePayload(Uint8List.fromList(talkData.content)); processCompletePayload.forEach((element) { talkData.content = element; - talkDataRepository.addTalkData(talkData); + talkDataRepository.addTalkData( + TalkDataModel( + talkData: talkData, + ), + ); }); } @@ -138,7 +143,11 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle // // 转pcm数据 // List pcmBytes = G711().convertList(g711Data); // talkData.content = pcmBytes; - talkDataRepository.addTalkData(talkData); + talkDataRepository.addTalkData( + TalkDataModel( + talkData: talkData, + ), + ); } catch (e) { print('Error decoding G.711 to PCM: $e'); } diff --git a/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart index 20dc1d2f..d1c83e77 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart @@ -20,7 +20,7 @@ import '../../star_chart_manage.dart'; class UdpTalkExpectHandler extends ScpMessageBaseHandle implements ScpMessageHandler { - final TalkViewState talkViewState = Get.put(TalkViewLogic()).state; + // final TalkViewState talkViewState = Get.put(TalkViewLogic()).state; @override void handleReq(ScpMessage scpMessage) { @@ -40,7 +40,7 @@ class UdpTalkExpectHandler extends ScpMessageBaseHandle startChartManage.stopTalkExpectMessageTimer(); // 停止发送对讲请求 startChartManage.stopCallRequestMessageTimer(); - talkViewState.rotateAngle.value = talkExpectResp.rotate ?? 0; + // talkViewState.rotateAngle.value = talkExpectResp.rotate ?? 0; // 收到预期数据的应答后,代表建立了连接,启动通话保持的监听 // 启动通话保持监听定时器(用来判断如果x秒内没有收到通话保持则执行的操作); talkePingOverTimeTimerManager.start(); diff --git a/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart index be2f4565..974c8f25 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart @@ -7,7 +7,9 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:get/get.dart'; import 'package:star_lock/appRouters.dart'; import 'package:star_lock/app_settings/app_settings.dart'; +import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart'; import 'package:star_lock/talk/starChart/constant/message_type_constant.dart'; +import 'package:star_lock/talk/starChart/constant/talk_constant.dart'; import 'package:star_lock/talk/starChart/constant/talk_status.dart'; import 'package:star_lock/talk/starChart/entity/scp_message.dart'; import 'package:star_lock/talk/starChart/handle/scp_message_base_handle.dart'; @@ -16,6 +18,7 @@ import 'package:star_lock/talk/starChart/proto/gateway_reset.pb.dart'; import 'package:star_lock/talk/starChart/proto/generic.pb.dart'; import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart'; import 'package:star_lock/talk/starChart/proto/talk_request.pb.dart'; +import 'package:star_lock/tools/commonDataManage.dart'; import 'package:star_lock/tools/push/xs_jPhush.dart'; import 'package:star_lock/tools/storage.dart'; import 'package:star_lock/translations/current_locale_tool.dart'; @@ -30,7 +33,6 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle @override void handleReq(ScpMessage scpMessage) async { - final currentTime = DateTime.now().millisecondsSinceEpoch; // 确保与上次请求间隔至少1秒 if (currentTime - _lastRequestTime < 1000) { @@ -105,18 +107,16 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle _showTalkRequestNotification(talkObjectName: talkObjectName); // 设置为等待接听状态 talkStatus.setPassiveCallWaitingAnswer(); - // 收到呼叫请求,跳转到接听页面 if (startChartManage - .getDefaultTalkExpect() - .videoType - .indexOf(VideoTypeE.H264) == - -1) { + .getDefaultTalkExpect() + .videoType + .contains(VideoTypeE.H264)) { Get.toNamed( - Routers.starChartTalkView, + Routers.h264WebView, ); } else { Get.toNamed( - Routers.h264WebView, + Routers.starChartTalkView, ); } } @@ -189,7 +189,24 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle } void _handleSendExpect() { - // 修改预期数据并启动发送预期数据定时器,在收到回复时停止 - startChartManage.sendOnlyImageVideoTalkExpectData(); + final LockListInfoItemEntity currentKeyInfo = + CommonDataManage().currentKeyInfo; + final isH264 = currentKeyInfo.lockFeature?.isH264 == 1; + final isMJpeg = currentKeyInfo.lockFeature?.isMJpeg == 1; + + // 优先使用H264,其次是MJPEG + if (isH264) { + // 锁支持H264,发送H264视频和G711音频期望 + startChartManage.sendOnlyH264VideoTalkExpectData(); + print('锁支持H264,发送H264视频格式期望数据'); + } else if (isMJpeg) { + // 锁只支持MJPEG,发送图像视频和G711音频期望 + startChartManage.sendOnlyImageVideoTalkExpectData(); + print('锁不支持H264,支持MJPEG,发送MJPEG视频格式期望数据'); + } else { + // 默认使用图像视频 + startChartManage.sendOnlyImageVideoTalkExpectData(); + print('锁不支持H264和MJPEG,默认发送图像视频格式期望数据'); + } } } diff --git a/lib/talk/starChart/handle/other/h264_frame_handler.dart b/lib/talk/starChart/handle/other/h264_frame_handler.dart index cd2f8bc2..903524da 100644 --- a/lib/talk/starChart/handle/other/h264_frame_handler.dart +++ b/lib/talk/starChart/handle/other/h264_frame_handler.dart @@ -3,15 +3,20 @@ import 'dart:typed_data'; import 'package:flutter/services.dart'; import 'package:star_lock/app_settings/app_settings.dart'; +import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart'; +import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart'; import '../../proto/talk_data_h264_frame.pb.dart'; class H264FrameHandler { + final void Function(TalkDataModel frameData) onCompleteFrame; - final void Function(List frameData) onCompleteFrame; + // 只记录最近一个I帧的序号 + int _lastProcessedIFrameSeq = -1; H264FrameHandler({required this.onCompleteFrame}); - void handleFrame(TalkDataH264Frame frame) { - onCompleteFrame(frame.frameData); + void handleFrame(TalkDataH264Frame frame, TalkData talkData) { + onCompleteFrame( + TalkDataModel(talkData: talkData, talkDataH264Frame: frame)); } } diff --git a/lib/talk/starChart/handle/other/talk_data_model.dart b/lib/talk/starChart/handle/other/talk_data_model.dart new file mode 100644 index 00000000..a84f1d3d --- /dev/null +++ b/lib/talk/starChart/handle/other/talk_data_model.dart @@ -0,0 +1,9 @@ +import 'package:star_lock/talk/starChart/proto/talk_data.pbserver.dart'; +import 'package:star_lock/talk/starChart/proto/talk_data_h264_frame.pb.dart'; + +class TalkDataModel { + TalkData? talkData; + TalkDataH264Frame? talkDataH264Frame; + + TalkDataModel({required this.talkData, this.talkDataH264Frame}); +} diff --git a/lib/talk/starChart/handle/other/talk_data_repository.dart b/lib/talk/starChart/handle/other/talk_data_repository.dart index 062835cb..587a481f 100644 --- a/lib/talk/starChart/handle/other/talk_data_repository.dart +++ b/lib/talk/starChart/handle/other/talk_data_repository.dart @@ -1,9 +1,10 @@ import 'dart:async'; +import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart'; import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart'; class TalkDataRepository { TalkDataRepository._() { - _talkDataStreamController = StreamController.broadcast( + _talkDataStreamController = StreamController.broadcast( onListen: () { _isListening = true; }, @@ -18,13 +19,13 @@ class TalkDataRepository { static TalkDataRepository get instance => _instance; - late final StreamController _talkDataStreamController; + late final StreamController _talkDataStreamController; bool _isListening = false; // 直接返回原始流,不做转换 - Stream get talkDataStream => _talkDataStreamController.stream; + Stream get talkDataStream => _talkDataStreamController.stream; - void addTalkData(TalkData talkData) { + void addTalkData(TalkDataModel talkData) { if (_isListening) { _talkDataStreamController.add(talkData); } diff --git a/lib/talk/starChart/handle/scp_message_base_handle.dart b/lib/talk/starChart/handle/scp_message_base_handle.dart index 11328f46..495db676 100644 --- a/lib/talk/starChart/handle/scp_message_base_handle.dart +++ b/lib/talk/starChart/handle/scp_message_base_handle.dart @@ -15,6 +15,7 @@ import 'package:star_lock/talk/starChart/constant/payload_type_constant.dart'; import 'package:star_lock/talk/starChart/constant/udp_constant.dart'; import 'package:star_lock/talk/starChart/entity/scp_message.dart'; import 'package:star_lock/talk/starChart/handle/other/h264_frame_handler.dart'; +import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart'; import 'package:star_lock/talk/starChart/handle/other/talk_data_repository.dart'; import 'package:star_lock/talk/starChart/handle/other/talke_data_over_time_timer_manager.dart'; @@ -55,10 +56,10 @@ class ScpMessageBaseHandle { // 处理出完整帧数据后的回调 final H264FrameHandler frameHandler = - H264FrameHandler(onCompleteFrame: (frameData) { + H264FrameHandler(onCompleteFrame: (TalkDataModel talkDataModel) { // 处理完整的帧数据 TalkDataRepository.instance.addTalkData( - TalkData(contentType: TalkData_ContentTypeE.H264, content: frameData), + talkDataModel, ); }); @@ -71,6 +72,7 @@ class ScpMessageBaseHandle { messageId: scpMessage.MessageId!, ); } + // 回复失败消息 void replyErrorMessage(ScpMessage scpMessage) { startChartManage.sendGenericRespErrorMessage( diff --git a/lib/talk/starChart/star_chart_manage.dart b/lib/talk/starChart/star_chart_manage.dart index 7ffed8e5..a8f9ad5f 100644 --- a/lib/talk/starChart/star_chart_manage.dart +++ b/lib/talk/starChart/star_chart_manage.dart @@ -113,7 +113,7 @@ class StartChartManage { final int _maxPayloadSize = 8 * 1024; // 分包大小 // 默认通话的期望数据格式 - TalkExpectReq _defaultTalkExpect = TalkConstant.ImageExpect; + TalkExpectReq _defaultTalkExpect = TalkConstant.H264Expect; String relayPeerId = ''; // 中继peerId @@ -227,20 +227,6 @@ class StartChartManage { /// 设置数据接收回调 _onReceiveData(_udpSocket!, Get.context!); - - // //ToDo: 增加对讲调试、正式可删除 - // // 每秒重置数据速率 - // Timer.periodic(Duration(seconds: 1), (Timer t) { - // UdpTalkDataHandler().resetDataRates(); - // // 更新调试信息 - // Provider.of(Get.context!, listen: false) - // .updateDebugInfo( - // UdpTalkDataHandler().getLastRecvDataRate() ~/ 1024, // 转换为KB - // UdpTalkDataHandler().getLastRecvPacketCount(), - // UdpTalkDataHandler().getLastSendDataRate() ~/ 1024, // 转换为KB - // UdpTalkDataHandler().getLastSendPacketCount(), - // ); - // }); }).catchError((error) { _log(text: 'Failed to bind UDP socket: $error'); }); @@ -1145,7 +1131,7 @@ class StartChartManage { } void reSetDefaultTalkExpect() { - _defaultTalkExpect = TalkConstant.ImageExpect; + _defaultTalkExpect = TalkConstant.H264Expect; } TalkExpectReq getDefaultTalkExpect() { @@ -1163,12 +1149,27 @@ class StartChartManage { } /// 修改预期接收到的数据 - void sendImageVideoAndG711AudioTalkExpectData() { - final talkExpectReq = TalkConstant.ImageExpect; + void sendOnlyH264VideoTalkExpectData() { + final talkExpectReq = TalkExpectReq( + videoType: [VideoTypeE.H264], + audioType: [], + ); changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer( talkExpect: talkExpectReq); } + /// 修改预期接收到的数据 + void sendImageVideoAndG711AudioTalkExpectData() { + changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer( + talkExpect: TalkConstant.ImageExpect); + } + + /// 修改预期接收到的数据 + void sendH264VideoAndG711AudioTalkExpectData() { + changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer( + talkExpect: TalkConstant.H264Expect); + } + /// 发送远程开锁 void sendRemoteUnLockMessage({ required String bluetoothDeviceName, diff --git a/lib/talk/starChart/views/talkView/talk_view_logic.dart b/lib/talk/starChart/views/talkView/talk_view_logic.dart index 779e44e9..a4a850ba 100644 --- a/lib/talk/starChart/views/talkView/talk_view_logic.dart +++ b/lib/talk/starChart/views/talkView/talk_view_logic.dart @@ -23,6 +23,7 @@ import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart'; import 'package:star_lock/network/api_repository.dart'; import 'package:star_lock/talk/call/g711.dart'; import 'package:star_lock/talk/starChart/constant/talk_status.dart'; +import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart'; import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart'; import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart'; import 'package:star_lock/talk/starChart/star_chart_manage.dart'; @@ -96,31 +97,15 @@ class TalkViewLogic extends BaseGetXController { // 监听音视频数据流 void _startListenTalkData() { - state.talkDataRepository.talkDataStream.listen((TalkData talkData) async { - final contentType = talkData.contentType; + state.talkDataRepository.talkDataStream + .listen((TalkDataModel talkDataModel) async { + final talkData = talkDataModel.talkData; + final contentType = talkData!.contentType; final currentTime = DateTime.now().millisecondsSinceEpoch; // 判断数据类型,进行分发处理 switch (contentType) { case TalkData_ContentTypeE.G711: - // // 第一帧到达时记录开始时间 - // if (_isFirstAudioFrame) { - // _startAudioTime = currentTime; - // _isFirstAudioFrame = false; - // } - - // 计算音频延迟 - final expectedTime = _startAudioTime + talkData.durationMs; - final audioDelay = currentTime - expectedTime; - - // 如果延迟太大,清空缓冲区并直接播放 - if (audioDelay > 500) { - state.audioBuffer.clear(); - if (state.isOpenVoice.value) { - _playAudioFrames(); - } - return; - } if (state.audioBuffer.length >= audioBufferSize) { state.audioBuffer.removeAt(0); // 丢弃最旧的数据 } @@ -195,7 +180,6 @@ class TalkViewLogic extends BaseGetXController { // _frameCount = 0; // _lastFpsUpdateTime = currentTime; // } - } else { // AppLog.log('⚠️ 帧未找到缓存 - Key: $cacheKey'); state.videoBuffer.removeAt(oldestIndex); // 移除无法播放的帧 @@ -497,7 +481,6 @@ class TalkViewLogic extends BaseGetXController { _initAudioRecorder(); requestPermissions(); - } @override diff --git a/lib/talk/starChart/webView/h264_web_logic.dart b/lib/talk/starChart/webView/h264_web_logic.dart index fdbaad3f..626dff04 100644 --- a/lib/talk/starChart/webView/h264_web_logic.dart +++ b/lib/talk/starChart/webView/h264_web_logic.dart @@ -25,7 +25,9 @@ import 'package:star_lock/network/api_repository.dart'; import 'package:star_lock/talk/call/g711.dart'; import 'package:star_lock/talk/starChart/constant/talk_status.dart'; import 'package:star_lock/talk/starChart/handle/other/packet_loss_statistics.dart'; +import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart'; import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart'; +import 'package:star_lock/talk/starChart/proto/talk_data_h264_frame.pb.dart'; import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart'; import 'package:star_lock/talk/starChart/star_chart_manage.dart'; import 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart'; @@ -44,6 +46,9 @@ class H264WebViewLogic extends BaseGetXController { // 添加模拟数据相关变量 static const int CHUNK_SIZE = 4096; Timer? _mockDataTimer; + int _startAudioTime = 0; // 开始播放时间戳 + int audioBufferSize = 2; // 音频默认缓冲2帧 + // 定义音频帧缓冲和发送函数 final List _bufferedAudioFrames = []; final Queue> _frameBuffer = Queue>(); @@ -51,7 +56,6 @@ class H264WebViewLogic extends BaseGetXController { @override void onInit() { - super.onInit(); // 初始化 WebView 控制器 state.webViewController = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) @@ -63,18 +67,40 @@ class H264WebViewLogic extends BaseGetXController { }, ); - state.isShowLoading.value = true; - // 加载本地 HTML - _loadLocalHtml(); + super.onInit(); // 创建流数据监听 _createFramesStreamListen(); - // playLocalTestVideo(); _startListenTalkStatus(); state.talkStatus.value = state.startChartTalkStatus.status; // 初始化音频播放器 _initFlutterPcmSound(); + // 初始化录音控制器 _initAudioRecorder(); + + // 加载本地 HTML + _loadLocalHtml(); + + // playLocalTestVideo(); + + requestPermissions(); + } + + Future requestPermissions() async { + // 申请存储权限 + var storageStatus = await Permission.storage.request(); + // 申请录音权限 + var microphoneStatus = await Permission.microphone.request(); + + if (storageStatus.isGranted && microphoneStatus.isGranted) { + print("Permissions granted"); + } else { + print("Permissions denied"); + // 如果权限被拒绝,可以提示用户或跳转到设置页面 + if (await Permission.storage.isPermanentlyDenied) { + openAppSettings(); // 跳转到应用设置页面 + } + } } /// 初始化音频录制器 @@ -96,16 +122,37 @@ class H264WebViewLogic extends BaseGetXController { } void _createFramesStreamListen() async { - state.talkDataRepository.talkDataStream.listen((TalkData event) async { - // 添加新帧到缓冲区 - _frameBuffer.add(event.content); + state.talkDataRepository.talkDataStream + .listen((TalkDataModel talkDataModel) async { + final talkData = talkDataModel.talkData; + final contentType = talkData!.contentType; + final currentTime = DateTime.now().millisecondsSinceEpoch; - // 当缓冲区超过最大容量时,发送最早的帧并移除 - while (_frameBuffer.length > FRAME_BUFFER_SIZE) { - if (_frameBuffer.isNotEmpty) { - final frame = _frameBuffer.removeFirst(); - await _sendBufferedData(frame); - } + // 判断数据类型,进行分发处理 + switch (contentType) { + case TalkData_ContentTypeE.G711: + if (state.audioBuffer.length >= audioBufferSize) { + state.audioBuffer.removeAt(0); // 丢弃最旧的数据 + } + state.audioBuffer.add(talkData); // 添加新数据 + // 添加音频播放逻辑,与视频类似 + _playAudioFrames(); + break; + case TalkData_ContentTypeE.H264: + // // 添加新帧到缓冲区 + _frameBuffer.add(talkData.content); + + // 当缓冲区超过最大容量时,发送最早的帧并移除 + while (_frameBuffer.length > FRAME_BUFFER_SIZE) { + if (_frameBuffer.isNotEmpty) { + final frame = _frameBuffer.removeFirst(); + await _sendBufferedData(frame); + } + if (state.isShowLoading.isTrue) { + state.isShowLoading.value = false; + } + } + break; } }); } @@ -134,6 +181,51 @@ class H264WebViewLogic extends BaseGetXController { // } // } + // 新增:音频帧播放逻辑 + void _playAudioFrames() { + // 如果缓冲区为空或未达到目标大小,不进行播放 + // 音频缓冲区要求更小,以减少延迟 + if (state.audioBuffer.isEmpty || + state.audioBuffer.length < audioBufferSize) { + return; + } + + // 找出时间戳最小的音频帧 + TalkData? oldestFrame; + int oldestIndex = -1; + for (int i = 0; i < state.audioBuffer.length; i++) { + if (oldestFrame == null || + state.audioBuffer[i].durationMs < oldestFrame.durationMs) { + oldestFrame = state.audioBuffer[i]; + oldestIndex = i; + } + } + + // 确保找到了有效帧 + if (oldestFrame != null && oldestIndex != -1) { + if (state.isOpenVoice.value) { + // 播放音频 + _playAudioData(oldestFrame); + } + state.audioBuffer.removeAt(oldestIndex); + } + } + + /// 播放音频数据 + void _playAudioData(TalkData talkData) async { + if (state.isOpenVoice.value) { + final list = + G711().decodeAndDenoise(talkData.content, true, 8000, 300, 150); + // // 将 PCM 数据转换为 PcmArrayInt16 + final PcmArrayInt16 fromList = PcmArrayInt16.fromList(list); + FlutterPcmSound.feed(fromList); + if (!state.isPlaying.value) { + FlutterPcmSound.play(); + state.isPlaying.value = true; + } + } + } + /// 加载html文件 Future _loadLocalHtml() async { // 加载 HTML 文件内容 @@ -186,10 +278,10 @@ class H264WebViewLogic extends BaseGetXController { Timer.periodic(const Duration(seconds: 1), (Timer t) { if (state.isShowLoading.isFalse) { state.oneMinuteTime.value++; - if (state.oneMinuteTime.value >= 60) { - t.cancel(); // 取消定时器 - state.oneMinuteTime.value = 0; - } + // if (state.oneMinuteTime.value >= 60) { + // t.cancel(); // 取消定时器 + // state.oneMinuteTime.value = 0; + // } } }); break; @@ -321,7 +413,7 @@ class H264WebViewLogic extends BaseGetXController { } // 首先应用固定增益提升基础音量 - List amplifiedFrame = _applyGain(frame, 1.6); + List amplifiedFrame = _applyGain(frame, 1.8); // 编码为G711数据 List encodedData = G711Tool.encode(amplifiedFrame, 0); // 0表示A-law _bufferedAudioFrames.addAll(encodedData); @@ -409,7 +501,7 @@ class H264WebViewLogic extends BaseGetXController { }); final LockSetInfoEntity lockSetInfoEntity = - await ApiRepository.to.getLockSettingInfoData( + await ApiRepository.to.getLockSettingInfoData( lockId: lockId.toString(), ); if (lockSetInfoEntity.errorCode!.codeIsSuccessful) { @@ -427,7 +519,6 @@ class H264WebViewLogic extends BaseGetXController { } } - @override void dispose() { // _mockDataTimer?.cancel(); diff --git a/lib/talk/starChart/webView/h264_web_view_state.dart b/lib/talk/starChart/webView/h264_web_view_state.dart index 2ae11041..671e805c 100644 --- a/lib/talk/starChart/webView/h264_web_view_state.dart +++ b/lib/talk/starChart/webView/h264_web_view_state.dart @@ -5,6 +5,7 @@ import 'package:flutter_voice_processor/flutter_voice_processor.dart'; import 'package:get/get.dart'; import 'package:star_lock/talk/starChart/constant/talk_status.dart'; import 'package:star_lock/talk/starChart/handle/other/talk_data_repository.dart'; +import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart'; import 'package:star_lock/talk/starChart/status/star_chart_talk_status.dart'; import 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart'; import 'package:webview_flutter/webview_flutter.dart'; @@ -49,4 +50,6 @@ class H264WebViewState { RxInt rotateAngle = 0.obs; // 旋转角度(以弧度为单位) RxBool hasAudioData = false.obs; // 是否有音频数据 RxInt lastAudioTimestamp = 0.obs; // 最后接收到的音频数据的时间戳 + List audioBuffer = [].obs; + RxBool isPlaying = false.obs; // 是否开始播放 } From f10351626ab455c01d9cc4abb0e67ad01e900a19 Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 25 Apr 2025 10:21:05 +0800 Subject: [PATCH 002/151] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0=E5=8E=9F?= =?UTF-8?q?=E7=94=9F=E6=8F=92=E4=BB=B6=E8=A7=A3=E7=A0=81=E7=9A=84=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E3=80=81=E5=A2=9E=E5=8A=A0h264=E3=80=81mjpeg=E5=88=87?= =?UTF-8?q?=E6=8D=A2=E7=9A=84debug=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/appRouters.dart | 2 + .../lockDetail/lockDetail_logic.dart | 19 +- .../lockDetail/lockDetail_page.dart | 58 +- .../lockDetail/lockDetail_state.dart | 11 +- lib/network/api_provider.dart | 2 +- lib/network/api_repository.dart | 2 +- .../handle/impl/udp_talk_data_handler.dart | 3 - .../handle/other/h264_frame_handler.dart | 3 - .../handle/other/packet_loss_statistics.dart | 84 +- lib/talk/starChart/star_chart_manage.dart | 2 +- .../native/talk_view_native_decode_logic.dart | 798 ++++++++++++++++++ .../native/talk_view_native_decode_page.dart | 557 ++++++++++++ .../native/talk_view_native_decode_state.dart | 109 +++ .../views/talkView/talk_view_logic.dart | 37 +- .../starChart/webView/h264_web_logic.dart | 20 +- pubspec.yaml | 3 +- 16 files changed, 1672 insertions(+), 38 deletions(-) create mode 100644 lib/talk/starChart/views/native/talk_view_native_decode_logic.dart create mode 100644 lib/talk/starChart/views/native/talk_view_native_decode_page.dart create mode 100644 lib/talk/starChart/views/native/talk_view_native_decode_state.dart diff --git a/lib/appRouters.dart b/lib/appRouters.dart index e1fa7672..4de686eb 100755 --- a/lib/appRouters.dart +++ b/lib/appRouters.dart @@ -60,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/starChart/views/native/talk_view_native_decode_page.dart'; import 'package:star_lock/talk/starChart/views/talkView/talk_view_page.dart'; import 'package:star_lock/talk/starChart/webView/h264_web_view.dart'; @@ -1197,6 +1198,7 @@ abstract class AppRouters { page: () => const DoubleLockLinkPage()), GetPage( name: Routers.starChartTalkView, page: () => const TalkViewPage()), + // GetPage(name: Routers.h264WebView, page: () => TalkViewNativeDecodePage()), GetPage(name: Routers.h264WebView, page: () => H264WebView()), ]; } diff --git a/lib/main/lockDetail/lockDetail/lockDetail_logic.dart b/lib/main/lockDetail/lockDetail/lockDetail_logic.dart index c03a2753..72683016 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_logic.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_logic.dart @@ -15,6 +15,8 @@ import 'package:star_lock/main/lockDetail/lockDetail/device_network_info.dart'; import 'package:star_lock/main/lockDetail/lockSet/lockTime/getServerDatetime_entity.dart'; import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart'; import 'package:star_lock/talk/starChart/constant/talk_status.dart'; +import 'package:star_lock/talk/starChart/handle/other/packet_loss_statistics.dart'; +import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart'; import 'package:star_lock/talk/starChart/star_chart_manage.dart'; import 'package:star_lock/tools/bugly/bugly_tool.dart'; import 'package:star_lock/tools/throttler.dart'; @@ -564,7 +566,7 @@ class LockDetailLogic extends BaseGetXController { // 获取手机联网token,根据锁设置里面获取的开锁时是否联网来判断是否调用这个接口 Future getLockNetToken() async { final LockNetTokenEntity entity = await ApiRepository.to - .getLockNetToken(lockId: state.keyInfos.value.lockId.toString()); + .getLockNetToken(lockId: state.keyInfos.value.lockId!); if (entity.errorCode!.codeIsSuccessful) { state.lockNetToken = entity.data!.token!.toString(); // AppLog.log('从服务器获取联网token:${state.lockNetToken}'); @@ -769,12 +771,12 @@ class LockDetailLogic extends BaseGetXController { if (catEyeConfig.isNotEmpty && catEyeConfig.length > 0 && catEyeConfig[0].catEyeMode != 0) { - if (StartChartManage().lockNetworkInfo.wifiName == null || - StartChartManage().lockNetworkInfo.wifiName == '') { + if ((StartChartManage().lockNetworkInfo.wifiName == null || + StartChartManage().lockNetworkInfo.wifiName == '') ) { showToast('设备未配网'.tr); return; } - + PacketLossStatistics().reset(); // 发送监控id StartChartManage().startCallRequestMessageTimer( ToPeerId: StartChartManage().lockNetworkInfo.peerId ?? ''); @@ -795,6 +797,15 @@ class LockDetailLogic extends BaseGetXController { @override void onInit() { super.onInit(); + + // 初始化开关状态为当前对讲视频模式 + final currentTalkExpect = StartChartManage().getDefaultTalkExpect(); + if (currentTalkExpect.videoType.contains(VideoTypeE.H264)) { + state.useH264Mode.value = true; + } else if (currentTalkExpect.videoType.contains(VideoTypeE.IMAGE)) { + state.useH264Mode.value = false; + } + state.LockSetChangeSetRefreshLockDetailWithTypeSubscription = eventBus .on() .listen((LockSetChangeSetRefreshLockDetailWithType event) { diff --git a/lib/main/lockDetail/lockDetail/lockDetail_page.dart b/lib/main/lockDetail/lockDetail/lockDetail_page.dart index 1040b5ba..93ea8f2f 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_page.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_page.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -88,7 +89,6 @@ class _LockDetailPageState extends State /// 路由订阅 AppRouteObserver().routeObserver.subscribe(this, ModalRoute.of(context)!); state.isOpenLockNeedOnline.refresh(); - } StreamSubscription? _lockRefreshLockDetailInfoDataEvent; @@ -507,6 +507,60 @@ class _LockDetailPageState extends State Widget skWidget() { return ListView( children: [ + // Container( + // padding: EdgeInsets.symmetric(vertical: 15, horizontal: 20), + // margin: EdgeInsets.only(top: 10, bottom: 10), + // decoration: BoxDecoration( + // color: Colors.white, + // borderRadius: BorderRadius.circular(10), + // boxShadow: [ + // BoxShadow( + // color: Colors.black.withOpacity(0.05), + // blurRadius: 5, + // offset: Offset(0, 2), + // ), + // ], + // ), + // child: Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // Text('对讲视频模式'.tr, + // style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + // Row( + // children: [ + // Text('mjpeg', + // style: TextStyle( + // fontSize: 14, + // color: !state.useH264Mode.value + // ? AppColors.mainColor + // : Colors.grey)), + // Obx(() => Switch( + // value: state.useH264Mode.value, + // activeColor: AppColors.mainColor, + // onChanged: (value) { + // state.useH264Mode.value = value; + // if (value) { + // // 使用H264模式 + // StartChartManage() + // .sendH264VideoAndG711AudioTalkExpectData(); + // } else { + // // 使用Image模式 + // StartChartManage() + // .sendImageVideoAndG711AudioTalkExpectData(); + // } + // }, + // )), + // Text('H264'.tr, + // style: TextStyle( + // fontSize: 14, + // color: state.useH264Mode.value + // ? AppColors.mainColor + // : Colors.grey)), + // ], + // ), + // ], + // ), + // ), Visibility( visible: (state.keyInfos.value.keyType == XSConstantMacro.keyTypeTime || @@ -1467,7 +1521,7 @@ class _LockDetailPageState extends State state.iSOpenLock.value = true; state.openLockBtnState.value = 1; state.animationController!.forward(); - // AppLog.log('点击开锁'); + AppLog.log('点击开锁'); if (isOpenLockNeedOnline) { // 不需要联网 state.openDoorModel = 0; diff --git a/lib/main/lockDetail/lockDetail/lockDetail_state.dart b/lib/main/lockDetail/lockDetail/lockDetail_state.dart index 01178f52..a949b693 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_state.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_state.dart @@ -7,18 +7,18 @@ import 'package:star_lock/main/lockDetail/lockSet/lockSet/lockSetInfo_entity.dar import '../../../blue/io_reply.dart'; import '../../lockMian/entity/lockListInfo_entity.dart'; - class LockDetailState { Rx keyInfos = LockListInfoItemEntity().obs; final Rx lockSetInfoData = LockSetInfoData().obs; late StreamSubscription replySubscription; - StreamSubscription? lockSetOpenOrCloseCheckInRefreshLockDetailWithAttendanceEvent; + StreamSubscription? + lockSetOpenOrCloseCheckInRefreshLockDetailWithAttendanceEvent; StreamSubscription? LockSetChangeSetRefreshLockDetailWithTypeSubscription; StreamSubscription? DetailLockInfo; StreamSubscription? SuccessfulDistributionNetworkEvent; String lockNetToken = '0'; - int differentialTime = 0;// 服务器时间与本地时间差值 + int differentialTime = 0; // 服务器时间与本地时间差值 bool isHaveNetwork = true; int lockUserNo = 0; int senderUserId = 0; @@ -41,7 +41,7 @@ class LockDetailState { RxBool bottomBtnisEable = true.obs; // 是否不可用 用于限制底部按钮是否可用 RxBool openDoorBtnisUneable = true.obs; // 当钥匙状态不能使用的情况下开锁按钮禁止使用,默认可用 - int openDoorModel = 0;// 离线开门0, 在线开门2 离线关门32 在线关门34 + int openDoorModel = 0; // 离线开门0, 在线开门2 离线关门32 在线关门34 //过渡动画控制器 AnimationController? animationController; @@ -58,6 +58,9 @@ class LockDetailState { int logCountPage = 10; // 蓝牙记录一页多少个 RxInt nextAuthTime = 0.obs; // 下次认证时间 + // 视频编码模式选择开关状态 + RxBool useH264Mode = true.obs; // true表示使用H264模式,false表示使用Image模式 + // LockDetailState() { // Map map = Get.arguments; // lockCount = map["lockCount"]; diff --git a/lib/network/api_provider.dart b/lib/network/api_provider.dart index 4cad97cc..01aabb37 100755 --- a/lib/network/api_provider.dart +++ b/lib/network/api_provider.dart @@ -353,7 +353,7 @@ class ApiProvider extends BaseProvider { ); // 获取手机联网token - Future getLockNetToken(String lockId) => post( + Future getLockNetToken(int lockId) => post( getLockNetTokenURL.toUrl, jsonEncode({ 'lockId': lockId, diff --git a/lib/network/api_repository.dart b/lib/network/api_repository.dart index b5e09c33..86d3c604 100755 --- a/lib/network/api_repository.dart +++ b/lib/network/api_repository.dart @@ -325,7 +325,7 @@ class ApiRepository { } // 获取手机联网token - Future getLockNetToken({required String lockId}) async { + Future getLockNetToken({required int lockId}) async { final res = await apiProvider.getLockNetToken(lockId); return LockNetTokenEntity.fromJson(res.body); } diff --git a/lib/talk/starChart/handle/impl/udp_talk_data_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_data_handler.dart index 44705624..028ffb73 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_data_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_data_handler.dart @@ -62,9 +62,6 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle int? spTotal, int? spIndex, int? messageId}) { - // 获取统计信息 - final stats = PacketLossStatistics().getStatistics(); - // _asyncLog('丢包统计: $stats'); // _asyncLog( // '分包数据:messageId:$messageId [$spIndex/$spTotal] PayloadLength:$PayloadLength'); if (messageType == MessageTypeConstant.RealTimeData) { diff --git a/lib/talk/starChart/handle/other/h264_frame_handler.dart b/lib/talk/starChart/handle/other/h264_frame_handler.dart index 903524da..face6b68 100644 --- a/lib/talk/starChart/handle/other/h264_frame_handler.dart +++ b/lib/talk/starChart/handle/other/h264_frame_handler.dart @@ -10,9 +10,6 @@ import '../../proto/talk_data_h264_frame.pb.dart'; class H264FrameHandler { final void Function(TalkDataModel frameData) onCompleteFrame; - // 只记录最近一个I帧的序号 - int _lastProcessedIFrameSeq = -1; - H264FrameHandler({required this.onCompleteFrame}); void handleFrame(TalkDataH264Frame frame, TalkData talkData) { diff --git a/lib/talk/starChart/handle/other/packet_loss_statistics.dart b/lib/talk/starChart/handle/other/packet_loss_statistics.dart index 34dd2282..8aabf6fd 100644 --- a/lib/talk/starChart/handle/other/packet_loss_statistics.dart +++ b/lib/talk/starChart/handle/other/packet_loss_statistics.dart @@ -10,6 +10,10 @@ class PacketLossStatistics { // key: messageId, value: {totalPackets, receivedPackets} final Map _packetsMap = HashMap(); + // 配置参数 + int _maxCapacity = 300; // 最大容量为300条记录 + int _timeoutMs = 30000; // 默认超时时间为30秒 + // 统计信息 int _totalMessages = 0; // 总消息数 int _lostMessages = 0; // 丢包的消息数 @@ -18,10 +22,19 @@ class PacketLossStatistics { // 记录分包数据 void recordPacket(int messageId, int currentIndex, int totalPackets) { + // 定期清理超时记录 + _cleanupExpiredPackets(); + + // 检查容量限制 + _checkCapacityLimit(); + if (!_packetsMap.containsKey(messageId)) { _packetsMap[messageId] = PacketInfo(totalPackets); _totalMessages++; _totalPackets += totalPackets; + } else { + // 更新时间戳 + _packetsMap[messageId]!.timestamp = DateTime.now().millisecondsSinceEpoch; } _packetsMap[messageId]!.receivedPackets.add(currentIndex); @@ -32,6 +45,51 @@ class PacketLossStatistics { } } + // 清理超时的记录 + void _cleanupExpiredPackets() { + final currentTime = DateTime.now().millisecondsSinceEpoch; + final expiredMessageIds = []; + + _packetsMap.forEach((messageId, info) { + // 如果记录超时,添加到待清理列表 + if (currentTime - info.timestamp > _timeoutMs) { + expiredMessageIds.add(messageId); + + // 统计丢包 + _lostMessages++; + _lostPackets += (info.totalPackets - info.receivedPackets.length); + } + }); + + // 移除超时记录 + for (var messageId in expiredMessageIds) { + _packetsMap.remove(messageId); + } + } + + // 检查并确保不超过最大容量 + void _checkCapacityLimit() { + if (_packetsMap.length <= _maxCapacity) { + return; + } + + // 如果超过容量限制,按时间戳排序并删除最旧的记录 + var entries = _packetsMap.entries.toList() + ..sort((a, b) => a.value.timestamp.compareTo(b.value.timestamp)); + + // 计算需要移除的数量(移除25%的旧记录,至少保证有一定空间) + int removeCount = (_packetsMap.length * 0.25).ceil(); + + // 移除并统计丢包 + for (int i = 0; i < removeCount && i < entries.length; i++) { + var entry = entries[i]; + _lostMessages++; + _lostPackets += + (entry.value.totalPackets - entry.value.receivedPackets.length); + _packetsMap.remove(entry.key); + } + } + // 检查丢包情况 void _checkPacketLoss(int messageId) { final info = _packetsMap[messageId]!; @@ -62,6 +120,28 @@ class PacketLossStatistics { return PacketLossInfo(messageLossRate, packetLossRate); } + // Getter和Setter,允许外部调整参数 + int get maxCapacity => _maxCapacity; + set maxCapacity(int value) { + if (value > 0) { + _maxCapacity = value; + // 设置新容量后立即检查 + _checkCapacityLimit(); + } + } + + int get timeoutMs => _timeoutMs; + set timeoutMs(int value) { + if (value > 0) { + _timeoutMs = value; + // 设置新超时后立即清理 + _cleanupExpiredPackets(); + } + } + + // 获取当前未完成记录数 + int get pendingRecordsCount => _packetsMap.length; + // 重置统计数据 void reset() { _packetsMap.clear(); @@ -76,8 +156,10 @@ class PacketLossStatistics { class PacketInfo { final int totalPackets; final Set receivedPackets = HashSet(); + int timestamp; // 添加时间戳字段,记录最后更新时间 - PacketInfo(this.totalPackets); + PacketInfo(this.totalPackets) + : timestamp = DateTime.now().millisecondsSinceEpoch; } // 丢包统计信息类 diff --git a/lib/talk/starChart/star_chart_manage.dart b/lib/talk/starChart/star_chart_manage.dart index a8f9ad5f..66de4aeb 100644 --- a/lib/talk/starChart/star_chart_manage.dart +++ b/lib/talk/starChart/star_chart_manage.dart @@ -1226,7 +1226,7 @@ class StartChartManage { await Storage.removerStarChartRegisterNodeInfo(); // 关闭udp服务 closeUdpSocket(); - PacketLossStatistics().reset(); + } /// 重置数据 diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart new file mode 100644 index 00000000..cf76414e --- /dev/null +++ b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart @@ -0,0 +1,798 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:ui' as ui; +import 'dart:math'; // Import the math package to use sqrt + +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.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:gallery_saver/gallery_saver.dart'; +import 'package:get/get.dart'; +import 'package:image_gallery_saver/image_gallery_saver.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:star_lock/app_settings/app_settings.dart'; +import 'package:star_lock/login/login/entity/LoginEntity.dart'; +import 'package:star_lock/main/lockDetail/lockDetail/lockDetail_logic.dart'; +import 'package:star_lock/main/lockDetail/lockDetail/lockDetail_state.dart'; +import 'package:star_lock/main/lockDetail/lockDetail/lockNetToken_entity.dart'; +import 'package:star_lock/main/lockDetail/lockSet/lockSet/lockSetInfo_entity.dart'; +import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart'; +import 'package:star_lock/network/api_repository.dart'; +import 'package:star_lock/talk/call/callTalk.dart'; +import 'package:star_lock/talk/call/g711.dart'; +import 'package:star_lock/talk/starChart/constant/talk_status.dart'; +import 'package:star_lock/talk/starChart/handle/other/packet_loss_statistics.dart'; +import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart'; +import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart'; +import 'package:star_lock/talk/starChart/proto/talk_data_h264_frame.pb.dart'; +import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart'; +import 'package:star_lock/talk/starChart/star_chart_manage.dart'; +import 'package:star_lock/talk/starChart/views/native/talk_view_native_decode_state.dart'; +import 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart'; +import 'package:star_lock/tools/G711Tool.dart'; +import 'package:star_lock/tools/bugly/bugly_tool.dart'; +import 'package:video_decode_plugin/video_decode_plugin.dart'; + +import '../../../../tools/baseGetXController.dart'; + +class TalkViewNativeDecodeLogic extends BaseGetXController { + final TalkViewNativeDecodeState state = TalkViewNativeDecodeState(); + + final LockDetailState lockDetailState = Get.put(LockDetailLogic()).state; + + int bufferSize = 25; // 初始化为默认大小 + + int audioBufferSize = 2; // 音频默认缓冲2帧 + + // 定义音频帧缓冲和发送函数 + final List _bufferedAudioFrames = []; + + // 添加监听状态和订阅引用 + bool _isListening = false; + StreamSubscription? _streamSubscription; + + Timer? _batchProcessTimer; + + // 添加一个集合来跟踪已成功解码的I帧序号 + final Set _decodedIFrames = {}; + + // 初始化视频解码器 + Future _initVideoDecoder() async { + try { + // 创建解码器配置 + final config = VideoDecoderConfig( + width: 864, + // 实际视频宽度 + height: 480, + frameRate: 25, + // 明确设置帧率 + // 增大缓冲区大小 + codecType: CodecType.h264, + // 编解码类型 + isDebug: true, + ); + + // 初始化解码器并获取textureId + final textureId = await VideoDecodePlugin.initDecoder(config); + + if (textureId != null) { + state.textureId.value = textureId; + AppLog.log('视频解码器初始化成功:textureId=$textureId'); + + // 设置帧回调 + VideoDecodePlugin.setFrameCallback(_onFrameAvailable); + + // 设置状态回调 + VideoDecodePlugin.setStateCallbackForTexture( + textureId, _onDecoderStateChanged); + + // 启动FPS监测 + startFpsMonitoring(); + } else { + AppLog.log('视频解码器初始化失败'); + } + } catch (e) { + AppLog.log('初始化视频解码器错误: $e'); + // 如果初始化失败,延迟后重试 + await Future.delayed(const Duration(seconds: 2)); + if (!Get.isRegistered()) { + return; // 如果控制器已经被销毁,不再重试 + } + _initVideoDecoder(); // 重试初始化 + } + } + + // 添加帧可用回调 + void _onFrameAvailable(int textureId) {} + + // 解码器状态变化回调 + void _onDecoderStateChanged( + int textureId, DecoderState decoderState, Map stats) { + String stateText; + switch (decoderState) { + case DecoderState.initializing: + state.isLoading.value = true; + stateText = "初始化中"; + break; + case DecoderState.ready: + stateText = "准备就绪"; + break; + case DecoderState.rendering: + stateText = "渲染中"; + state.isLoading.value = false; + break; + case DecoderState.error: + stateText = "出错"; + // 获取错误信息 + final errorMessage = stats['errorMessage'] as String?; + if (errorMessage != null) { + AppLog.log("解码器错误: $errorMessage"); + } + break; + case DecoderState.released: + stateText = "已释放"; + break; + default: + stateText = "未知状态"; + } + + // 更新统计信息 + if (stats.isNotEmpty) { + // 获取丢包率统计信息 + final PacketLossInfo packetLossInfo = + PacketLossStatistics().getStatistics(); + + // 更新FPS + // state.decoderFps.value = (stats['fps'] as num?)?.toDouble() ?? 0.0; + // 更新解码器详细信息 + state.renderedFrameCount.value = (stats['renderedFrames'] as int?) ?? 0; + state.totalFrames.value = (stats['totalFrames'] as int?) ?? 0; + state.droppedFrames.value = (stats['droppedFrames'] as int?) ?? 0; + state.hasSentIDR.value = (stats['hasSentIDR'] as bool?) ?? false; + state.hasSentSPS.value = (stats['hasSentSPS'] as bool?) ?? false; + state.hasSentPPS.value = (stats['hasSentPPS'] as bool?) ?? false; + state.keyFrameInterval.value = (stats['keyFrameInterval'] as int?) ?? 0; + state.decodingJitterMs.value = (stats['decodingJitterMs'] as int?) ?? 0; + + // 更新状态数据 + state.messageLossRate.value = packetLossInfo.messageLossRate; + state.packetLossRate.value = packetLossInfo.packetLossRate; + state.lastPacketStatsUpdateTime.value = + DateTime.now().millisecondsSinceEpoch; + } + } + + /// 初始化音频播放器 + void _initFlutterPcmSound() { + const int sampleRate = 8000; + FlutterPcmSound.setLogLevel(LogLevel.none); + FlutterPcmSound.setup(sampleRate: sampleRate, channelCount: 1); + // 设置 feed 阈值 + if (Platform.isAndroid) { + FlutterPcmSound.setFeedThreshold(1024); // Android 平台的特殊处理 + } else { + FlutterPcmSound.setFeedThreshold(2000); // 非 Android 平台的处理 + } + } + + /// 挂断 + void udpHangUpAction() async { + if (state.talkStatus.value == TalkStatus.answeredSuccessfully) { + // 如果是通话中就挂断 + StartChartManage().startTalkHangupMessageTimer(); + } else { + // 拒绝 + StartChartManage().startTalkRejectMessageTimer(); + } + if (state.textureId.value != null) { + VideoDecodePlugin.releaseDecoderForTexture(state.textureId.value!); + } + VideoDecodePlugin.releaseAllDecoders(); + Get.back(); + } + + // 发起接听命令 + void initiateAnswerCommand() { + StartChartManage().startTalkAcceptTimer(); + } + + // 监听音视频数据流 + void _startListenTalkData() { + // 防止重复监听 + if (_isListening) { + AppLog.log("已经存在数据流监听,避免重复监听"); + return; + } + + AppLog.log("==== 启动新的数据流监听 ===="); + _isListening = true; + + _streamSubscription = state.talkDataRepository.talkDataStream + .listen((TalkDataModel talkDataModel) async { + final talkData = talkDataModel.talkData; + final talkDataH264Frame = talkDataModel.talkDataH264Frame; + final contentType = talkData!.contentType; + + // 判断数据类型,进行分发处理 + switch (contentType) { + case TalkData_ContentTypeE.G711: + if (state.audioBuffer.length >= audioBufferSize) { + state.audioBuffer.removeAt(0); // 丢弃最旧的数据 + } + state.audioBuffer.add(talkData); // 添加新数据 + // 添加音频播放逻辑,与视频类似 + _playAudioFrames(); + break; + case TalkData_ContentTypeE.H264: + // 添加到视频帧缓冲区,而不是直接处理 + // _processH264Frame(talkData, talkDataH264Frame!); + // 直接处理H264视频帧 + _processH264Frame(talkData, talkDataH264Frame!); + + // 记录关键调试信息 + if (talkDataH264Frame!.frameType == TalkDataH264Frame_FrameTypeE.I) { + AppLog.log( + '帧序号${talkDataH264Frame.frameSeq};帧类型:${talkDataH264Frame.frameType.toString()};时间戳:${DateTime.now().millisecondsSinceEpoch}'); + } + break; + } + }); + } + + // 处理H264视频帧 + Future _processH264Frame( + TalkData talkData, TalkDataH264Frame frameInfo) async { + // 检查解码器是否已初始化 + if (state.textureId.value == null) { + // 可以记录日志或尝试初始化解码器 + AppLog.log('解码器尚未初始化,尝试重新初始化...'); + await _initVideoDecoder(); + + // 如果仍未初始化成功,则丢弃此帧 + if (state.textureId.value == null) { + return; + } + } + // 获取P帧对应的I帧序号 + final frameSeqI = frameInfo.frameSeqI; + + // P帧检查:如果依赖的I帧未解码成功,直接丢弃 + if (frameInfo.frameType == TalkDataH264Frame_FrameTypeE.P && + !_decodedIFrames.contains(frameSeqI)) { + AppLog.log('丢弃P帧: 依赖的I帧(${frameSeqI})尚未解码, P帧序号: ${frameInfo.frameSeq}'); + return; + } + + // 从talkData中提取H264帧数据 + final Uint8List frameData = Uint8List.fromList(talkData.content); + + // 确定帧类型 + final FrameType frameType = + frameInfo.frameType == TalkDataH264Frame_FrameTypeE.I + ? FrameType.iFrame + : FrameType.pFrame; + + // 将帧数据交给解码器处理 + try { + final bool result = + await VideoDecodePlugin.decodeFrame(frameData, frameType); + // 如果是I帧且成功解码,将其序号加入已解码I帧集合 + if (frameInfo.frameType == TalkDataH264Frame_FrameTypeE.I && result) { + _decodedIFrames.add(frameInfo.frameSeq); + // 限制集合大小,避免内存泄漏 + if (_decodedIFrames.length > 30) { + _decodedIFrames.remove(_decodedIFrames.first); + } + } + } catch (e) { + AppLog.log('解码帧错误: $e, 帧序号: ${frameInfo.frameSeq}'); + } + } + + // 新增:音频帧播放逻辑 + void _playAudioFrames() { + // 如果缓冲区为空或未达到目标大小,不进行播放 + // 音频缓冲区要求更小,以减少延迟 + if (state.audioBuffer.isEmpty || + state.audioBuffer.length < audioBufferSize) { + return; + } + + // 找出时间戳最小的音频帧 + TalkData? oldestFrame; + int oldestIndex = -1; + for (int i = 0; i < state.audioBuffer.length; i++) { + if (oldestFrame == null || + state.audioBuffer[i].durationMs < oldestFrame.durationMs) { + oldestFrame = state.audioBuffer[i]; + oldestIndex = i; + } + } + + // 确保找到了有效帧 + if (oldestFrame != null && oldestIndex != -1) { + if (state.isOpenVoice.value) { + // 播放音频 + _playAudioData(oldestFrame); + } + state.audioBuffer.removeAt(oldestIndex); + } + } + + /// 监听对讲状态 + 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; + case TalkStatus.answeredSuccessfully: + state.oneMinuteTimeTimer?.cancel(); // 取消旧定时器 + state.oneMinuteTimeTimer ??= + Timer.periodic(const Duration(seconds: 1), (Timer t) { + if (state.isLoading.isFalse) { + state.oneMinuteTime.value++; + } + }); + break; + default: + // 其他状态的处理 + break; + } + }); + } + + /// 播放音频数据 + void _playAudioData(TalkData talkData) async { + if (state.isOpenVoice.value) { + final list = + G711().decodeAndDenoise(talkData.content, true, 8000, 300, 150); + // // 将 PCM 数据转换为 PcmArrayInt16 + final PcmArrayInt16 fromList = PcmArrayInt16.fromList(list); + FlutterPcmSound.feed(fromList); + if (!state.isPlaying.value) { + FlutterPcmSound.play(); + state.isPlaying.value = true; + } + } + } + + /// 停止播放音频 + void _stopPlayG711Data() async { + await FlutterPcmSound.pause(); + await FlutterPcmSound.stop(); + await FlutterPcmSound.clear(); + } + + /// 获取权限状态 + 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(); + } + } + + Future requestPermissions() async { + // 申请存储权限 + var storageStatus = await Permission.storage.request(); + // 申请录音权限 + var microphoneStatus = await Permission.microphone.request(); + + if (storageStatus.isGranted && microphoneStatus.isGranted) { + print("Permissions granted"); + } else { + print("Permissions denied"); + // 如果权限被拒绝,可以提示用户或跳转到设置页面 + if (await Permission.storage.isPermanentlyDenied) { + openAppSettings(); // 跳转到应用设置页面 + } + } + } + + Future startRecording() async {} + + Future stopRecording() async {} + + @override + void onReady() { + super.onReady(); + } + + @override + void onInit() { + super.onInit(); + + // 启动监听音视频数据流 + _startListenTalkData(); + // 启动监听对讲状态 + _startListenTalkStatus(); + // 在没有监听成功之前赋值一遍状态 + // *** 由于页面会在状态变化之后才会初始化,导致识别不到最新的状态,在这里手动赋值 *** + state.talkStatus.value = state.startChartTalkStatus.status; + + // 初始化音频播放器 + _initFlutterPcmSound(); + + // 启动播放定时器 + // _startPlayback(); + + // 初始化录音控制器 + _initAudioRecorder(); + + requestPermissions(); + + // 初始化视频解码器 + _initVideoDecoder(); + } + + @override + void onClose() { + _stopPlayG711Data(); // 停止播放音频 + + state.audioBuffer.clear(); // 清空音频缓冲区 + + state.oneMinuteTimeTimer?.cancel(); + state.oneMinuteTimeTimer = null; + + // 停止播放音频 + stopProcessingAudio(); + + state.oneMinuteTimeTimer?.cancel(); // 取消旧定时器 + state.oneMinuteTimeTimer = null; // 取消旧定时器 + state.oneMinuteTime.value = 0; + + // 释放视频解码器资源 + if (state.textureId.value != null) { + VideoDecodePlugin.releaseDecoder(); + state.textureId.value = null; + } + + // 取消数据流监听 + _streamSubscription?.cancel(); + _isListening = false; + + // 停止FPS监测 + stopFpsMonitoring(); + // 重置期望数据 + StartChartManage().reSetDefaultTalkExpect(); + VideoDecodePlugin.releaseAllDecoders(); + + // 取消批处理定时器 + _batchProcessTimer?.cancel(); + _batchProcessTimer = null; + + // 清空已解码I帧集合 + _decodedIFrames.clear(); + + super.onClose(); + } + + /// 处理无效通话状态 + void _handleInvalidTalkStatus() { + // 停止播放音频 + _stopPlayG711Data(); + stopProcessingAudio(); + } + + /// 更新发送预期数据 + void updateTalkExpect() { + TalkExpectReq talkExpectReq = TalkExpectReq(); + state.isOpenVoice.value = !state.isOpenVoice.value; + if (!state.isOpenVoice.value) { + talkExpectReq = TalkExpectReq( + videoType: [VideoTypeE.IMAGE], + audioType: [], + ); + showToast('已静音'.tr); + } else { + talkExpectReq = TalkExpectReq( + videoType: [VideoTypeE.IMAGE], + audioType: [AudioTypeE.G711], + ); + } + + /// 修改发送预期数据 + StartChartManage().changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer( + talkExpect: talkExpectReq); + } + + /// 截图并保存到相册 + 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'); + showToast('截图已保存到相册'.tr); + } catch (e) { + AppLog.log('截图失败: $e'); + } + } + + // 远程开锁 + Future remoteOpenLock() async { + final lockPeerId = StartChartManage().lockPeerId; + final lockListPeerId = StartChartManage().lockListPeerId; + int lockId = lockDetailState.keyInfos.value.lockId ?? 0; + + // 如果锁列表获取到peerId,代表有多个锁,使用锁列表的peerId + // 从列表中遍历出对应的peerId + lockListPeerId.forEach((element) { + if (element.network?.peerId == lockPeerId) { + lockId = element.lockId ?? 0; + } + }); + + final LockSetInfoEntity lockSetInfoEntity = + await ApiRepository.to.getLockSettingInfoData( + lockId: lockId.toString(), + ); + if (lockSetInfoEntity.errorCode!.codeIsSuccessful) { + if (lockSetInfoEntity.data?.lockFeature?.remoteUnlock == 1 && + lockSetInfoEntity.data?.lockSettingInfo?.remoteUnlock == 1) { + final LoginEntity entity = await ApiRepository.to + .remoteOpenLock(lockId: lockId.toString(), timeOut: 60); + if (entity.errorCode!.codeIsSuccessful) { + showToast('已开锁'.tr); + StartChartManage().lockListPeerId = []; + } + } else { + showToast('该锁的远程开锁功能未启用'.tr); + } + } + } + + /// 初始化音频录制器 + void _initAudioRecorder() { + state.voiceProcessor = VoiceProcessor.instance; + } + + //开始录音 + Future startProcessingAudio() async { + try { + if (await state.voiceProcessor?.hasRecordAudioPermission() ?? false) { + await state.voiceProcessor?.start(state.frameLength, state.sampleRate); + final bool? isRecording = await state.voiceProcessor?.isRecording(); + state.isRecordingAudio.value = isRecording!; + state.startRecordingAudioTime.value = DateTime.now(); + + // 增加录音帧监听器和错误监听器 + state.voiceProcessor + ?.addFrameListeners([_onFrame]); + state.voiceProcessor?.addErrorListener(_onError); + } else { + // state.errorMessage.value = 'Recording permission not granted'; + } + } on PlatformException catch (ex) { + // state.errorMessage.value = 'Failed to start recorder: $ex'; + } + state.isOpenVoice.value = false; + } + + /// 停止录音 + Future stopProcessingAudio() async { + try { + await state.voiceProcessor?.stop(); + state.voiceProcessor?.removeFrameListener(_onFrame); + state.udpSendDataFrameNumber = 0; + // 记录结束时间 + state.endRecordingAudioTime.value = DateTime.now(); + + // 计算录音的持续时间 + final Duration duration = state.endRecordingAudioTime.value + .difference(state.startRecordingAudioTime.value); + + state.recordingAudioTime.value = duration.inSeconds; + } on PlatformException catch (ex) { + // state.errorMessage.value = 'Failed to stop recorder: $ex'; + } finally { + final bool? isRecording = await state.voiceProcessor?.isRecording(); + state.isRecordingAudio.value = isRecording!; + state.isOpenVoice.value = true; + } + } + +// 音频帧处理 + Future _onFrame(List frame) async { + // 添加最大缓冲限制 + if (_bufferedAudioFrames.length > state.frameLength * 3) { + _bufferedAudioFrames.clear(); // 清空过多积累的数据 + return; + } + + // 首先应用固定增益提升基础音量 + List amplifiedFrame = _applyGain(frame, 1.6); + // 编码为G711数据 + List encodedData = G711Tool.encode(amplifiedFrame, 0); // 0表示A-law + _bufferedAudioFrames.addAll(encodedData); + // 使用相对时间戳 + final int ms = DateTime.now().millisecondsSinceEpoch % 1000000; // 使用循环时间戳 + int getFrameLength = state.frameLength; + if (Platform.isIOS) { + getFrameLength = state.frameLength * 2; + } + + // 添加发送间隔控制 + if (_bufferedAudioFrames.length >= state.frameLength) { + try { + await StartChartManage().sendTalkDataMessage( + talkData: TalkData( + content: _bufferedAudioFrames, + contentType: TalkData_ContentTypeE.G711, + durationMs: ms, + ), + ); + } finally { + _bufferedAudioFrames.clear(); // 确保清理缓冲区 + } + } else { + _bufferedAudioFrames.addAll(encodedData); + } + } + +// 错误监听 + void _onError(VoiceProcessorException error) { + AppLog.log(error.message!); + } + +// 添加音频增益处理方法 + List _applyGain(List pcmData, double gainFactor) { + List result = List.filled(pcmData.length, 0); + + for (int i = 0; i < pcmData.length; i++) { + // PCM数据通常是有符号的16位整数 + int sample = pcmData[i]; + + // 应用增益 + double amplified = sample * gainFactor; + + // 限制在有效范围内,防止溢出 + if (amplified > 32767) { + amplified = 32767; + } else if (amplified < -32768) { + amplified = -32768; + } + + result[i] = amplified.toInt(); + } + + return result; + } + + // 启动网络状态监测 + void startFpsMonitoring() { + // 确保只有一个计时器在运行 + stopFpsMonitoring(); + + // 初始化时间记录 + state.lastFpsUpdateTime.value = DateTime.now().millisecondsSinceEpoch; + + // 创建一个计时器,每秒更新一次丢包率和性能数据 + state.fpsTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + // 更新丢包率数据 + updatePacketLossStats(); + + // 分析性能数据 + _analyzePerformance(); + }); + } + + // 停止网络状态监测 + void stopFpsMonitoring() { + state.fpsTimer?.cancel(); + state.fpsTimer = null; + } + + // 日志记录方法 + void logMessage(String message) { + AppLog.log(message); + } + + // 更新丢包率统计数据 + void updatePacketLossStats() async { + try {} catch (e) { + logMessage('获取丢包率数据失败: $e'); + } + } + + // 添加性能分析方法 + void _analyzePerformance() { + final int now = DateTime.now().millisecondsSinceEpoch; + + // 如果是首次调用,初始化数据 + if (state.lastPerformanceCheck == 0) { + state.lastPerformanceCheck = now; + state.lastFrameCount = state.renderedFrameCount.value; + return; + } + + // 每秒分析一次性能 + if (now - state.lastPerformanceCheck >= 1000) { + // 计算过去一秒的实际帧率 + final int frameRendered = + state.renderedFrameCount.value - state.lastFrameCount; + final double actualFPS = + frameRendered * 1000 / (now - state.lastPerformanceCheck); + + // 计算丢帧率 + final double dropRate = state.droppedFrames.value / + (state.totalFrames.value > 0 ? state.totalFrames.value : 1) * + 100; + + // 计算当前解码器积压帧数 + final int pendingFrames = + state.totalFrames.value - state.renderedFrameCount.value; + + // 计算跟踪Map中的帧数(正在处理中的帧) + final int processingFrames = state.frameTracker.length; + + // 分析渲染瓶颈 + String performanceStatus = "正常"; + if (actualFPS < 15 && dropRate > 10) { + performanceStatus = "严重渲染瓶颈"; + } else if (actualFPS < 20 && dropRate > 5) { + performanceStatus = "轻微渲染瓶颈"; + } + + // 输出综合性能分析 + AppLog.log("性能分析: 实际帧率=${actualFPS.toStringAsFixed(1)}fps, " + + "丢帧率=${dropRate.toStringAsFixed(1)}%, " + + "待处理帧数=$pendingFrames, " + + "处理中帧数=$processingFrames, " + + "状态=$performanceStatus"); + + // 重置统计数据 + state.lastPerformanceCheck = now; + state.lastFrameCount = state.renderedFrameCount.value; + } + } +} diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_page.dart b/lib/talk/starChart/views/native/talk_view_native_decode_page.dart new file mode 100644 index 00000000..3172a6a6 --- /dev/null +++ b/lib/talk/starChart/views/native/talk_view_native_decode_page.dart @@ -0,0 +1,557 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:http/http.dart' as http; +import 'package:provider/provider.dart'; +import 'package:star_lock/flavors.dart'; +import 'package:star_lock/talk/call/callTalk.dart'; +import 'package:star_lock/talk/starChart/constant/talk_status.dart'; +import 'package:star_lock/talk/starChart/handle/impl/debug_Info_model.dart'; +import 'package:star_lock/talk/starChart/handle/impl/udp_talk_data_handler.dart'; +import 'package:star_lock/talk/starChart/views/native/talk_view_native_decode_logic.dart'; +import 'package:star_lock/talk/starChart/views/native/talk_view_native_decode_state.dart'; +import 'package:star_lock/talk/starChart/views/talkView/talk_view_logic.dart'; +import 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart'; +import 'package:video_decode_plugin/video_decode_plugin.dart'; + +import '../../../../app_settings/app_colors.dart'; +import '../../../../tools/showTFView.dart'; + +class TalkViewNativeDecodePage extends StatefulWidget { + const TalkViewNativeDecodePage({Key? key}) : super(key: key); + + @override + State createState() => + _TalkViewNativeDecodePageState(); +} + +class _TalkViewNativeDecodePageState extends State + with TickerProviderStateMixin { + final TalkViewNativeDecodeLogic logic = Get.put(TalkViewNativeDecodeLogic()); + final TalkViewNativeDecodeState state = + Get.find().state; + + @override + void initState() { + super.initState(); + + state.animationController = AnimationController( + vsync: this, // 确保使用的TickerProvider是当前Widget + duration: const Duration(seconds: 1), + ); + + state.animationController.repeat(); + //动画开始、结束、向前移动或向后移动时会调用StatusListener + state.animationController.addStatusListener((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 WillPopScope( + onWillPop: () async { + // 返回 false 表示禁止退出 + return false; + }, + child: SizedBox( + width: 1.sw, + height: 1.sh, + child: Stack( + alignment: Alignment.center, + children: [ + Obx( + () { + final double screenWidth = MediaQuery.of(context).size.width; + final double screenHeight = MediaQuery.of(context).size.height; + + final double logicalWidth = MediaQuery.of(context).size.width; + final double logicalHeight = MediaQuery.of(context).size.height; + final double devicePixelRatio = + MediaQuery.of(context).devicePixelRatio; + + // 计算物理像素值 + final double physicalWidth = logicalWidth * devicePixelRatio; + final double physicalHeight = logicalHeight * devicePixelRatio; + + // 旋转后的图片尺寸 + const int rotatedImageWidth = 480; // 原始高度 + const int rotatedImageHeight = 864; // 原始宽度 + + // 计算缩放比例 + final double scaleWidth = physicalWidth / rotatedImageWidth; + final double scaleHeight = physicalHeight / rotatedImageHeight; + max(scaleWidth, scaleHeight); // 选择较大的缩放比例 + + return state.isLoading.isTrue + ? Image.asset( + 'images/main/monitorBg.png', + width: screenWidth, + height: screenHeight, + fit: BoxFit.cover, + ) + : PopScope( + canPop: false, + child: RepaintBoundary( + key: state.globalKey, + child: SizedBox.expand( + child: RotatedBox( + // 解码器不支持硬件旋转,使用RotatedBox + quarterTurns: -1, + child: Texture( + textureId: state.textureId.value!, + filterQuality: FilterQuality.medium, + ), + ), + ), + ), + ); + }, + ), + Obx(() => state.isLoading.isTrue + ? Positioned( + bottom: 310.h, + child: Text( + '正在创建安全连接...'.tr, + style: TextStyle(color: Colors.black, fontSize: 26.sp), + )) + : Container()), + Obx(() => state.textureId.value != null && state.showFps.value + ? Positioned( + top: ScreenUtil().statusBarHeight + 10.h, + right: 20.w, + child: Container( + padding: + EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.h), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + borderRadius: BorderRadius.circular(5.h), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + // Text( + // 'FPS: ${state.decoderFps.value.toStringAsFixed(1)}', + // style: TextStyle( + // color: _getPacketLossColor( + // state.packetLossRate.value), + // fontSize: 20.sp, + // ), + // ), + Text( + '丢包率: ${state.packetLossRate.value.toStringAsFixed(1)}%', + style: TextStyle( + color: _getPacketLossColor( + state.packetLossRate.value), + fontSize: 20.sp, + ), + ), + Text( + '消息丢失: ${state.messageLossRate.value.toStringAsFixed(1)}%', + style: TextStyle( + color: _getPacketLossColor( + state.messageLossRate.value), + fontSize: 20.sp, + ), + ), + Divider( + color: Colors.white30, + height: 10.h, + thickness: 1), + Text( + '已渲染帧: ${state.renderedFrameCount.value}', + style: + TextStyle(color: Colors.white, fontSize: 18.sp), + ), + Text( + '总帧数: ${state.totalFrames.value}', + style: + TextStyle(color: Colors.white, fontSize: 18.sp), + ), + Text( + '丢弃帧: ${state.droppedFrames.value}', + style: + TextStyle(color: Colors.white, fontSize: 18.sp), + ), + Text( + 'IDR帧: ${state.hasSentIDR.value ? "已发送" : "未发送"}', + style: TextStyle( + color: state.hasSentIDR.value + ? Colors.green + : Colors.red, + fontSize: 18.sp), + ), + Text( + 'SPS: ${state.hasSentSPS.value ? "已发送" : "未发送"}', + style: TextStyle( + color: state.hasSentSPS.value + ? Colors.green + : Colors.red, + fontSize: 18.sp), + ), + Text( + 'PPS: ${state.hasSentPPS.value ? "已发送" : "未发送"}', + style: TextStyle( + color: state.hasSentPPS.value + ? Colors.green + : Colors.red, + fontSize: 18.sp), + ), + Text( + 'keyFrameInterval: ${state.keyFrameInterval.value}', + style: + TextStyle(color: Colors.green, fontSize: 18.sp), + ), + Text( + 'decodingJitterMs: ${state.decodingJitterMs.value}', + style: + TextStyle(color: Colors.green, fontSize: 18.sp), + ), + ], + ), + ), + ) + : Container()), + Obx(() => state.isLoading.isFalse && state.oneMinuteTime.value > 0 + ? Positioned( + top: ScreenUtil().statusBarHeight + 75.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), + ), + ], + ); + }, + ), + ) + : 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), + ], + ), + ), + ), + Obx(() => state.isLoading.isTrue + ? buildRotationTransition() + : Container()), + Obx(() => state.isLongPressing.value + ? Positioned( + top: 80.h, + left: 0, + right: 0, + child: Center( + child: Container( + padding: EdgeInsets.all(10.w), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.7), + borderRadius: BorderRadius.circular(10.w), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.mic, color: Colors.white, size: 24.w), + SizedBox(width: 10.w), + Text( + '正在说话...'.tr, + style: TextStyle( + fontSize: 20.sp, color: Colors.white), + ), + ], + ), + ), + ), + ) + : Container()), + ], + ), + ), + ); + } + + Widget bottomTopBtnWidget() { + return Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + // 打开关闭声音 + GestureDetector( + onTap: () { + if (state.talkStatus.value == TalkStatus.answeredSuccessfully) { + // 打开关闭声音 + logic.updateTalkExpect(); + } + }, + 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_monitoringOpenVoice.png') + : const AssetImage( + 'images/main/icon_lockDetail_monitoringCloseVoice.png'))), + ), + ), + SizedBox(width: 50.w), + // 截图 + GestureDetector( + onTap: () async { + if (state.talkStatus.value == TalkStatus.answeredSuccessfully) { + await logic.captureAndSavePng(); + } + }, + 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: () async { + logic.showToast('功能暂未开放'.tr); + // if ( + // state.talkStatus.value == TalkStatus.answeredSuccessfully) { + // if (state.isRecordingScreen.value) { + // await logic.stopRecording(); + // } else { + // await logic.startRecording(); + // } + // } + }, + 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'), + ), + ), + ), + ]); + } + + Widget bottomBottomBtnWidget() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // 接听 + Obx( + () => bottomBtnItemWidget( + getAnswerBtnImg(), + getAnswerBtnName(), + Colors.white, + longPress: () async { + if (state.talkStatus.value == TalkStatus.answeredSuccessfully) { + // 启动录音 + logic.startProcessingAudio(); + state.isLongPressing.value = true; + } + }, + longPressUp: () async { + // 停止录音 + logic.stopProcessingAudio(); + state.isLongPressing.value = false; + }, + onClick: () async { + if (state.talkStatus.value == + TalkStatus.passiveCallWaitingAnswer) { + // 接听 + logic.initiateAnswerCommand(); + } + }, + ), + ), + bottomBtnItemWidget( + 'images/main/icon_lockDetail_hangUp.png', '挂断'.tr, Colors.red, + onClick: () { + // 挂断 + logic.udpHangUpAction(); + }), + bottomBtnItemWidget( + 'images/main/icon_lockDetail_monitoringUnlock.png', + '开锁'.tr, + AppColors.mainColor, + onClick: () { + // if (state.talkStatus.value == TalkStatus.answeredSuccessfully && + // state.listData.value.length > 0) { + // logic.udpOpenDoorAction(); + // } + // if (UDPManage().remoteUnlock == 1) { + // logic.udpOpenDoorAction(); + // showDeletPasswordAlertDialog(context); + // } else { + // logic.showToast('请在锁设置中开启远程开锁'.tr); + // } + logic.remoteOpenLock(); + }, + ) + ]); + } + + String getAnswerBtnImg() { + switch (state.talkStatus.value) { + case TalkStatus.passiveCallWaitingAnswer: + return 'images/main/icon_lockDetail_monitoringAnswerCalls.png'; + case TalkStatus.answeredSuccessfully: + case TalkStatus.proactivelyCallWaitingAnswer: + return 'images/main/icon_lockDetail_monitoringUnTalkback.png'; + default: + return 'images/main/icon_lockDetail_monitoringAnswerCalls.png'; + } + } + + String getAnswerBtnName() { + switch (state.talkStatus.value) { + case TalkStatus.passiveCallWaitingAnswer: + return '接听'.tr; + case TalkStatus.proactivelyCallWaitingAnswer: + case TalkStatus.answeredSuccessfully: + 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: 160.w, + width: 140.w, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: wh, + height: wh, + constraints: BoxConstraints( + minWidth: 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), + Text( + name, + style: TextStyle(fontSize: 20.sp, color: Colors.white), + textAlign: TextAlign.center, // 当文本超出指定行数时,使用省略号表示 + maxLines: 2, // 设置最大行数为1 + ) + ], + ), + ), + ); + } + + // 根据丢包率返回对应的颜色 + Color _getPacketLossColor(double lossRate) { + if (lossRate < 1.0) { + return Colors.green; // 丢包率低于1%显示绿色 + } else if (lossRate < 5.0) { + return Colors.yellow; // 丢包率1%-5%显示黄色 + } else if (lossRate < 10.0) { + return Colors.orange; // 丢包率5%-10%显示橙色 + } else { + return Colors.red; // 丢包率高于10%显示红色 + } + } + + //旋转动画 + 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(); + CallTalk().finishAVData(); + super.dispose(); + } +} diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_state.dart b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart new file mode 100644 index 00000000..d98d5d34 --- /dev/null +++ b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart @@ -0,0 +1,109 @@ +import 'dart:async'; +import 'dart:typed_data'; +import 'dart:ui' as ui; +import 'dart:io'; + +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:get/get_rx/src/rx_types/rx_types.dart'; +import 'package:get/state_manager.dart'; +import 'package:network_info_plus/network_info_plus.dart'; +import 'package:star_lock/talk/starChart/constant/talk_status.dart'; +import 'package:star_lock/talk/starChart/handle/other/packet_loss_statistics.dart'; +import 'package:star_lock/talk/starChart/handle/other/talk_data_repository.dart'; +import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart'; +import 'package:star_lock/talk/starChart/status/star_chart_talk_status.dart'; +import 'package:video_decode_plugin/video_decode_plugin.dart'; + +import '../../../../tools/storage.dart'; + +enum NetworkStatus { + normal, // 0 + lagging, // 1 + delayed, // 2 + packetLoss // 3 +} + +class TalkViewNativeDecodeState { + // 视频源最大帧率限制 + static const int maxSourceFps = 25; // 视频源最高支持25fps + + 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(); + + RxList listAudioData = [].obs; //得到的音频流字节数据 + GlobalKey globalKey = GlobalKey(); + + Timer? oneMinuteTimeTimer; // 定时器超过60秒关闭当前界面 + RxInt oneMinuteTime = 0.obs; // 定时器秒数 + + // 定时器如果发送了接听的命令 而没收到回复就每秒重复发送10次 + late Timer answerTimer; + late Timer hangUpTimer; + late Timer openDoorTimer; + Timer? fpsTimer; + late AnimationController animationController; + + RxInt elapsedSeconds = 0.obs; + + // 星图对讲相关状态 + List audioBuffer = [].obs; + + RxBool isLoading = true.obs; // 是否在加载 + RxBool isPlaying = false.obs; // 是否开始播放 + Rx talkStatus = TalkStatus.none.obs; //星图对讲状态 + // 获取 startChartTalkStatus 的唯一实例 + final StartChartTalkStatus startChartTalkStatus = + StartChartTalkStatus.instance; + + // 通话数据流的单例流数据处理类 + final TalkDataRepository talkDataRepository = TalkDataRepository.instance; + + RxBool isOpenVoice = true.obs; // 是否打开声音 + RxBool isRecordingScreen = false.obs; // 是否录屏中 + RxBool isRecordingAudio = false.obs; // 是否录音中 + Rx startRecordingAudioTime = DateTime.now().obs; // 开始录音时间 + Rx endRecordingAudioTime = DateTime.now().obs; // 结束录音时间 + RxInt recordingAudioTime = 0.obs; // 录音时间持续时间 + late VoiceProcessor? voiceProcessor; // 音频处理器、录音 + final int frameLength = 320; //录音视频帧长度为640 + final int sampleRate = 8000; //录音频采样率为8000 + RxBool isLongPressing = false.obs; // 旋转角度(以弧度为单位) + // 视频解码器纹理ID + Rx textureId = Rx(null); + // FPS监测相关变量 + + RxInt lastFpsUpdateTime = 0.obs; // 上次FPS更新时间 + RxBool showFps = true.obs; // 是否显示FPS + // 丢包率统计相关变量 + RxDouble decoderFps = 0.0.obs; // 消息丢失率 + RxDouble messageLossRate = 0.0.obs; // 消息丢失率 + RxDouble packetLossRate = 0.0.obs; // 分包丢失率 + RxInt lastPacketStatsUpdateTime = 0.obs; // 上次更新丢包统计的时间 + + // 解码器详细统计信息 + RxInt renderedFrameCount = 0.obs; // 已渲染帧数 + RxInt totalFrames = 0.obs; // 总帧数 + RxInt droppedFrames = 0.obs; // 丢弃帧数 + RxBool hasSentIDR = false.obs; // 是否已发送IDR帧 + RxBool hasSentSPS = false.obs; // 是否已发送SPS + RxBool hasSentPPS = false.obs; // 是否已发送PPS + RxInt keyFrameInterval = 0.obs; // 关键帧间隔时间(ms) + RxInt decodingJitterMs = 0.obs; // 解码抖动时间(ms) + + // 性能分析变量 + int lastPerformanceCheck = 0; + int lastFrameCount = 0; + + // 帧跟踪Map,记录每个提交的帧,key为textureId_frameSeq + Map> frameTracker = {}; +} diff --git a/lib/talk/starChart/views/talkView/talk_view_logic.dart b/lib/talk/starChart/views/talkView/talk_view_logic.dart index a4a850ba..eaf051c1 100644 --- a/lib/talk/starChart/views/talkView/talk_view_logic.dart +++ b/lib/talk/starChart/views/talkView/talk_view_logic.dart @@ -45,12 +45,12 @@ class TalkViewLogic extends BaseGetXController { final int minAudioBufferSize = 1; // 音频最小缓冲1帧 final int maxAudioBufferSize = 3; // 音频最大缓冲3帧 int audioBufferSize = 2; // 音频默认缓冲2帧 - + bool _isFirstAudioFrame = true; // 是否是第一帧 // 添加开始时间记录 int _startTime = 0; // 开始播放时间戳 int _startAudioTime = 0; // 开始播放时间戳 bool _isFirstFrame = true; // 是否是第一帧 - bool _isFirstAudioFrame = true; // 是否是第一帧 + // 定义音频帧缓冲和发送函数 final List _bufferedAudioFrames = []; @@ -106,6 +106,24 @@ class TalkViewLogic extends BaseGetXController { // 判断数据类型,进行分发处理 switch (contentType) { case TalkData_ContentTypeE.G711: + // // 第一帧到达时记录开始时间 + if (_isFirstAudioFrame) { + _startAudioTime = currentTime; + _isFirstAudioFrame = false; + } + + // 计算音频延迟 + final expectedTime = _startAudioTime + talkData.durationMs; + final audioDelay = currentTime - expectedTime; + + // 如果延迟太大,清空缓冲区并直接播放 + if (audioDelay > 500) { + state.audioBuffer.clear(); + if (state.isOpenVoice.value) { + _playAudioFrames(); + } + return; + } if (state.audioBuffer.length >= audioBufferSize) { state.audioBuffer.removeAt(0); // 丢弃最旧的数据 } @@ -118,7 +136,7 @@ class TalkViewLogic extends BaseGetXController { if (_isFirstFrame) { _startTime = currentTime; _isFirstFrame = false; - AppLog.log('第一帧帧的时间戳:${talkData.durationMs}'); + // AppLog.log('第一帧帧的时间戳:${talkData.durationMs}'); } // AppLog.log('其他帧的时间戳:${talkData.durationMs}'); // 计算帧间间隔 @@ -366,19 +384,6 @@ class TalkViewLogic extends BaseGetXController { } } - // 获取手机联网token,根据锁设置里面获取的开锁时是否联网来判断是否调用这个接口 - Future _getLockNetToken() async { - final LockNetTokenEntity entity = await ApiRepository.to.getLockNetToken( - lockId: lockDetailState.keyInfos.value.lockId.toString()); - if (entity.errorCode!.codeIsSuccessful) { - lockDetailState.lockNetToken = entity.data!.token!.toString(); - AppLog.log('从服务器获取联网token:${lockDetailState.lockNetToken}'); - } else { - BuglyTool.uploadException( - message: '点击了需要联网开锁', detail: '点击了需要联网开锁 获取连网token失败', upload: true); - showToast('网络访问失败,请检查网络是否正常'.tr, something: () {}); - } - } /// 获取权限状态 Future getPermissionStatus() async { diff --git a/lib/talk/starChart/webView/h264_web_logic.dart b/lib/talk/starChart/webView/h264_web_logic.dart index 626dff04..4f5789c2 100644 --- a/lib/talk/starChart/webView/h264_web_logic.dart +++ b/lib/talk/starChart/webView/h264_web_logic.dart @@ -48,7 +48,7 @@ class H264WebViewLogic extends BaseGetXController { Timer? _mockDataTimer; int _startAudioTime = 0; // 开始播放时间戳 int audioBufferSize = 2; // 音频默认缓冲2帧 - + bool _isFirstAudioFrame = true; // 是否是第一帧 // 定义音频帧缓冲和发送函数 final List _bufferedAudioFrames = []; final Queue> _frameBuffer = Queue>(); @@ -131,6 +131,24 @@ class H264WebViewLogic extends BaseGetXController { // 判断数据类型,进行分发处理 switch (contentType) { case TalkData_ContentTypeE.G711: + // // 第一帧到达时记录开始时间 + if (_isFirstAudioFrame) { + _startAudioTime = currentTime; + _isFirstAudioFrame = false; + } + + // 计算音频延迟 + final expectedTime = _startAudioTime + talkData.durationMs; + final audioDelay = currentTime - expectedTime; + + // 如果延迟太大,清空缓冲区并直接播放 + if (audioDelay > 500) { + state.audioBuffer.clear(); + if (state.isOpenVoice.value) { + _playAudioFrames(); + } + return; + } if (state.audioBuffer.length >= audioBufferSize) { state.audioBuffer.removeAt(0); // 丢弃最旧的数据 } diff --git a/pubspec.yaml b/pubspec.yaml index f0f95559..a09deb7e 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -127,7 +127,8 @@ dependencies: sdk: flutter aliyun_face_plugin: path: aliyun_face_plugin - + video_decode_plugin: + path: ../video_decode_plugin flutter_localizations: sdk: flutter From df23655822f34c467f63e1116c5a10c6d23b71d3 Mon Sep 17 00:00:00 2001 From: liyi Date: Sat, 26 Apr 2025 15:15:27 +0800 Subject: [PATCH 003/151] =?UTF-8?q?fix:=E6=A2=B3=E7=90=86=E9=85=8D?= =?UTF-8?q?=E7=BD=91=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuringWifi_logic.dart | 528 ++++++++++++------ .../configuringWifi/configuringWifi_page.dart | 104 +++- .../configuringWifi_state.dart | 3 - .../wifiList/wifiList_logic.dart | 101 +++- .../wifiList/wifiList_page.dart | 145 +++-- 5 files changed, 626 insertions(+), 255 deletions(-) diff --git a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart index 25c8e43d..a940ca4e 100755 --- a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart +++ b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart @@ -37,15 +37,24 @@ import 'configuringWifi_state.dart'; class ConfiguringWifiLogic extends BaseGetXController { final ConfiguringWifiState state = ConfiguringWifiState(); + final int _configurationTimeout = 60; // 配网超时时间(秒) + /// 获取WiFi锁服务IP和端口 Future getWifiLockServiceIpAndPort() async { - final ConfiguringWifiEntity entity = - await ApiRepository.to.getWifiLockServiceIpAndPort(); - if (entity.errorCode! == 0) { - state.configuringWifiEntity.value = entity; + try { + final ConfiguringWifiEntity entity = + await ApiRepository.to.getWifiLockServiceIpAndPort(); + if (entity.errorCode! == 0) { + state.configuringWifiEntity.value = entity; + } else { + AppLog.log('获取WiFi锁服务IP和端口失败:${entity.errorCode}'); + } + } catch (e) { + AppLog.log('获取WiFi锁服务IP和端口异常:$e'); } } + /// 更新网络信息到服务器 void updateNetworkInfo({ required String peerId, required String wifiName, @@ -53,24 +62,36 @@ class ConfiguringWifiLogic extends BaseGetXController { required String deviceMac, required String networkMac, }) async { - final LoginEntity entity = await ApiRepository.to.settingDeviceNetwork( - deviceType: 2, - deviceMac: deviceMac, - wifiName: wifiName, - networkMac: networkMac, - secretKey: secretKey, - peerId: peerId, - ); - if (entity.errorCode!.codeIsSuccessful) { - // 设置锁的peerID - StartChartManage().lockNetworkInfo = DeviceNetworkInfo( + try { + final LoginEntity entity = await ApiRepository.to.settingDeviceNetwork( + deviceType: 2, + deviceMac: deviceMac, wifiName: wifiName, networkMac: networkMac, secretKey: secretKey, peerId: peerId, ); - await _getUploadLockSet(); + if (entity.errorCode!.codeIsSuccessful) { + // 设置锁的peerID + StartChartManage().lockNetworkInfo = DeviceNetworkInfo( + wifiName: wifiName, + networkMac: networkMac, + secretKey: secretKey, + peerId: peerId, + ); + + await _getUploadLockSet(); + } else { + dismissEasyLoading(); + showToast('网络配置失败,请重试'.tr); + state.sureBtnState.value = 0; + } + } catch (e) { + dismissEasyLoading(); + showToast('网络配置异常:${e.toString()}'.tr); + state.sureBtnState.value = 0; + AppLog.log('网络配置异常:$e'); } } @@ -84,81 +105,138 @@ class ConfiguringWifiLogic extends BaseGetXController { if (reply is GatewayConfiguringWifiResultReply) { _replySenderConfiguringWifiResult(reply); } + // wifi配网命令应答结果 if (reply is GatewayConfiguringWifiReply) { - _replySenderConfiguringWifiResult(reply); + _replySenderConfiguringWifi(reply); } if (reply is GatewayGetStatusReply) { _replyGatewayGetStatusReply(reply); } - // if (reply is GatewayGetStatusReply) { - // _replyStatusInfo(reply); - // } // 上传数据获取锁设置 if (reply is UpdataLockSetReply) { _replyUpdataLockSetReply(reply); } - AppLog.log('蓝牙回调处理完毕${EasyLoading.isShow}'); }); } - // WIFI配网结果 - Future _replySenderConfiguringWifiResult(Reply reply) async { + // WIFI配网操作结果处理 + Future _replySenderConfiguringWifi(Reply reply) async { final int status = reply.data[2]; - // state.sureBtnState.value = 0; - - // 取消loading超时定时器 - state.loadingTimer?.cancel(); - state.loadingTimer = null; switch (status) { case 0x00: - await Storage.removeLockNetWorkInfoCache(); - final int secretKeyJsonLength = (reply.data[4] << 8) + reply.data[3]; - - final List secretKeyList = - reply.data.sublist(5, 5 + secretKeyJsonLength); - String result = utf8String(secretKeyList); - // 解析 JSON 字符串为 Map - Map jsonMap = json.decode(result); - - // 提取 peerId - String? peerId = jsonMap['peerId']; - String? wifiName = jsonMap['wifiName']; - String? secretKey = jsonMap['secretKey']; - String? deviceMac = jsonMap['deviceMac']; - String? networkMac = jsonMap['networkMac']; - - /// 配网成功后,赋值锁的peerId - StartChartManage().lockPeerId = peerId ?? ''; - - state.isLoading.value = false; - // 保存到缓存 - await Storage.saveLockNetWorkInfo(jsonMap); - // 上报服务器 - updateNetworkInfo( - peerId: peerId ?? '', - wifiName: wifiName ?? '', - secretKey: secretKey ?? '', - deviceMac: deviceMac ?? '', - networkMac: networkMac ?? ''); - + AppLog.log('wifi配网命令回复结果:成功'); break; default: //失败 dismissEasyLoading(); // 关闭loading - cancelBlueConnetctToastTimer(); - if (state.loadingTimer != null) { - state.loadingTimer!.cancel(); - state.loadingTimer = null; - } - showToast('配网失败'.tr); state.isLoading.value = false; break; } } -// 辅助函数:美化 JSON 输出 + // WIFI配网结果处理 + Future _replySenderConfiguringWifiResult(Reply reply) async { + final int status = reply.data[2]; + + // 收到响应后,取消蓝牙超时计时器 + cancelBlueConnetctToastTimer(); + + switch (status) { + case 0x00: + // 配网成功 - 不关闭loading,保持状态直到全部完成 + await Storage.removeLockNetWorkInfoCache(); + + try { + final int secretKeyJsonLength = (reply.data[4] << 8) + reply.data[3]; + final List secretKeyList = + reply.data.sublist(5, 5 + secretKeyJsonLength); + String result = utf8String(secretKeyList); + + AppLog.log('解析配网信息: $result'); + + // 解析 JSON 字符串为 Map + Map jsonMap = json.decode(result); + + // 提取网络信息 + String? peerId = jsonMap['peerId']; + String? wifiName = jsonMap['wifiName']; + String? secretKey = jsonMap['secretKey']; + String? deviceMac = jsonMap['deviceMac']; + String? networkMac = jsonMap['networkMac']; + + // 验证关键字段 + if (peerId == null || + peerId.isEmpty || + secretKey == null || + secretKey.isEmpty) { + throw Exception('Missing required network information'); + } + + /// 配网成功后,赋值锁的peerId + StartChartManage().lockPeerId = peerId; + + // 保存到缓存 + await Storage.saveLockNetWorkInfo(jsonMap); + + // 上报服务器 - 注意: sureBtnState 状态将在 updateNetworkInfo 方法中或其回调中完成重置 + updateNetworkInfo( + peerId: peerId, + wifiName: wifiName ?? '', + secretKey: secretKey, + deviceMac: deviceMac ?? '', + networkMac: networkMac ?? ''); + } catch (e) { + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + showToast('解析配网信息失败,请重试'.tr); + state.sureBtnState.value = 0; // 确保重置状态 + AppLog.log('解析配网信息失败: $e'); + return; // 添加return阻止后续流程 + } + break; + + case 0x01: + // WiFi密码错误 + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + showToast('WiFi密码错误,请重新输入'.tr); + state.sureBtnState.value = 0; // 确保重置状态 + break; + + case 0x02: + // 找不到WiFi + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + showToast('找不到该WiFi网络,请确认WiFi名称正确'.tr); + state.sureBtnState.value = 0; // 确保重置状态 + break; + + case 0x03: + // 网络连接超时 + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + showToast('连接WiFi超时,请确保网络信号良好'.tr); + state.sureBtnState.value = 0; // 确保重置状态 + break; + + default: + // 其他错误 + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + showToast('配网失败 (错误码: $status),请重试'.tr); + state.sureBtnState.value = 0; // 确保重置状态 + break; + } + } + + // 辅助函数:美化 JSON 输出 String prettyPrintJson(String jsonString) { var jsonObject = json.decode(jsonString); return JsonEncoder.withIndent(' ').convert(jsonObject); @@ -168,63 +246,80 @@ class ConfiguringWifiLogic extends BaseGetXController { Future senderConfiguringWifiAction() async { AppLog.log('开始配网${EasyLoading.isShow}'); - if (state.isLoading.isTrue) { + if (state.sureBtnState.value == 1) { AppLog.log('正在配网中请勿重复点击'); return; } - if (state.wifiNameController.text.isEmpty) { - showToast('请输入wifi名称'.tr); + + // 获取网关配置信息 + try { + final GetGatewayConfigurationEntity entity = await ApiRepository.to + .getGatewayConfigurationNotLoading(timeout: _configurationTimeout); + if (entity.errorCode!.codeIsSuccessful) { + state.getGatewayConfigurationStr = entity.data ?? ''; + } else { + // showToast('获取网关配置失败,请重试'.tr); + AppLog.log('获取网关配置失败,请重试'); + return; + } + + // 判断是否登录账户 + final loginData = await Storage.getLoginData(); + if (loginData == null) { + AppLog.log('未检测到登录信息,请重新登录'.tr); + return; + } + + // 获取app用户的peerId + String appPeerId = loginData.starchart?.starchartId ?? ''; + if (appPeerId.isEmpty) { + AppLog.log('用户ID获取失败,请重新登录'.tr); + return; + } + + // 处理配置字符串 + if (state.getGatewayConfigurationStr.isNotEmpty) { + // 解析 JSON 字符串为 Map + Map jsonMap = + json.decode(state.getGatewayConfigurationStr); + + // 移除指定的键 + jsonMap.remove("starCloudUrl"); + jsonMap.remove("starLockPeerId"); + + // 追加新的键值对 + jsonMap['userPeerld'] = appPeerId; + + // 将 Map 转换回 JSON 字符串 + state.getGatewayConfigurationStr = + json.encode(jsonMap).replaceAll(',', ',\n'); + + // 确保格式化输出 + state.getGatewayConfigurationStr = + prettyPrintJson(state.getGatewayConfigurationStr); + } else { + // 如果为空,则直接赋值 + state.getGatewayConfigurationStr = "{\"userPeerld\": \"$appPeerId\"}"; + } + } catch (e) { + AppLog.log('网关配置准备失败:${e.toString()}'.tr); return; } - if (state.wifiPWDController.text.isEmpty) { - showToast('请输入WiFi密码'.tr); - return; - } - // if (state.sureBtnState.value == 1) { - // return; - // } - // state.sureBtnState.value = 1; + // 先设置sureBtnState状态,以禁用按钮 + state.sureBtnState.value = 1; - final GetGatewayConfigurationEntity entity = - await ApiRepository.to.getGatewayConfigurationNotLoading(timeout: 60); - if (entity.errorCode!.codeIsSuccessful) { - state.getGatewayConfigurationStr = entity.data ?? ''; + // 显示loading,如果已经显示则不再重复显示 + if (!EasyLoading.isShow) { + showEasyLoading(); } - // 判断是否登录账户 - final loginData = await Storage.getLoginData(); - - // 获取app用户的peerId - String appPeerId = loginData?.starchart?.starchartId ?? ''; - // 如果已有值,则追加 - if (state.getGatewayConfigurationStr.isNotEmpty) { - // 解析 JSON 字符串为 Map - Map jsonMap = - json.decode(state.getGatewayConfigurationStr); - - // 移除指定的键 - jsonMap.remove("starCloudUrl"); - jsonMap.remove("starLockPeerId"); - - // 追加新的键值对 - jsonMap['userPeerld'] = appPeerId; - - // 将 Map 转换回 JSON 字符串 - state.getGatewayConfigurationStr = - json.encode(jsonMap).replaceAll(',', ',\n'); - - // 确保格式化输出 - state.getGatewayConfigurationStr = - prettyPrintJson(state.getGatewayConfigurationStr); - } else { - // 如果为空,则直接赋值 - state.getGatewayConfigurationStr = "{\"userPeerld\": \"$appPeerId\"}"; - } - showEasyLoading(); + // 设置蓝牙操作超时处理 showBlueConnetctToastTimer(action: () { - dismissEasyLoading(); - state.isLoading.value = false; + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + state.sureBtnState.value = 0; // 连接超时时重置状态 }); // 发送配网指令 @@ -232,16 +327,27 @@ class ConfiguringWifiLogic extends BaseGetXController { BlueManage().connectDeviceName, (BluetoothConnectionState connectionState) async { if (connectionState == BluetoothConnectionState.connected) { - IoSenderManage.gatewayConfiguringWifiCommand( - ssid: state.wifiNameController.text, - password: state.wifiPWDController.text, - gatewayConfigurationStr: state.getGatewayConfigurationStr, - ); + try { + IoSenderManage.gatewayConfiguringWifiCommand( + ssid: state.wifiNameController.text, + password: state.wifiPWDController.text, + gatewayConfigurationStr: state.getGatewayConfigurationStr, + ); + // 注意:此处不要重置sureBtnState状态,等待配网结果回调 + } catch (e) { + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + cancelBlueConnetctToastTimer(); + state.sureBtnState.value = 0; // 发送命令失败时重置状态 + showToast('发送配网指令失败:${e.toString()}'.tr); + } } else if (connectionState == BluetoothConnectionState.disconnected) { - dismissEasyLoading(); + if (EasyLoading.isShow) { + dismissEasyLoading(); + } cancelBlueConnetctToastTimer(); - state.isLoading.value = false; - // state.sureBtnState.value = 0; + state.sureBtnState.value = 0; // 蓝牙断开时重置状态 if (state.ifCurrentScreen.value == true) { showBlueConnetctToast(); } @@ -249,7 +355,6 @@ class ConfiguringWifiLogic extends BaseGetXController { }, isAddEquipment: false, ); - state.isLoading.value = true; } // 获取设备状态 @@ -278,11 +383,15 @@ class ConfiguringWifiLogic extends BaseGetXController { final NetworkInfo _networkInfo = NetworkInfo(); Future getWifiName() async { - String ssid = ''; - ssid = (await _networkInfo.getWifiName())!; - ssid = ssid ?? ''; - ssid = ssid.replaceAll(r'"', ''); - return ssid ?? ''; + try { + String? ssid = await _networkInfo.getWifiName(); + ssid = ssid ?? ''; + ssid = ssid.replaceAll(r'"', ''); + return ssid; + } catch (e) { + AppLog.log('获取WiFi名称失败: $e'); + return ''; + } } ///定位权限 @@ -308,17 +417,12 @@ class ConfiguringWifiLogic extends BaseGetXController { getWifiLockServiceIpAndPort(); _initReplySubscription(); - // getDevicesStatusAction(); - } - - @override - void onInit() { - super.onInit(); } @override void onClose() { _replySubscription.cancel(); + cancelBlueConnetctToastTimer(); // 确保取消蓝牙超时计时器 super.onClose(); } @@ -330,8 +434,6 @@ class ConfiguringWifiLogic extends BaseGetXController { switch (status) { case 0x00: //成功 - // state.sureBtnState.value = 0; - final GetGatewayInfoModel gatewayModel = GetGatewayInfoModel(); // 网关MAC地址 int index = 3; @@ -372,70 +474,108 @@ class ConfiguringWifiLogic extends BaseGetXController { default: //失败 dismissEasyLoading(); - showToast('配网失败'.tr); - if (state.loadingTimer != null) { - state.loadingTimer!.cancel(); - state.loadingTimer = null; - } + showToast('获取设备状态失败'.tr); break; } } // 上传数据获取设置 Future _getUploadLockSet() async { - showEasyLoading(); + // 保持已有的loading状态,不再重新显示loading showBlueConnetctToastTimer(action: () { - dismissEasyLoading(); + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + state.sureBtnState.value = 0; }); - final List? token = await Storage.getStringList(saveBlueToken); - final List getTokenList = changeStringListToIntList(token!); - - await _uploadLockSet(getTokenList); + try { + final List? token = await Storage.getStringList(saveBlueToken); + if (token == null || token.isEmpty) { + throw Exception('Token is empty'); + } + final List getTokenList = changeStringListToIntList(token); + await _uploadLockSet(getTokenList); + } catch (e) { + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + cancelBlueConnetctToastTimer(); + showToast('获取设置失败:${e.toString()}'.tr); + state.sureBtnState.value = 0; + } } // 公共的上传锁设置 Future _uploadLockSet(List token) async { - final List? privateKey = - await Storage.getStringList(saveBluePrivateKey); - final List getPrivateKeyList = changeStringListToIntList(privateKey!); + try { + final List? privateKey = + await Storage.getStringList(saveBluePrivateKey); + if (privateKey == null || privateKey.isEmpty) { + throw Exception('Private key is empty'); + } + final List getPrivateKeyList = changeStringListToIntList(privateKey); - final List? signKey = await Storage.getStringList(saveBlueSignKey); - final List signKeyDataList = changeStringListToIntList(signKey!); + final List? signKey = + await Storage.getStringList(saveBlueSignKey); + if (signKey == null || signKey.isEmpty) { + throw Exception('Sign key is empty'); + } + final List signKeyDataList = changeStringListToIntList(signKey); - IoSenderManage.updataLockSetCommand( - lockID: BlueManage().connectDeviceName, - userID: await Storage.getUid(), - token: token, - needAuthor: 1, - signKey: signKeyDataList, - privateKey: getPrivateKeyList); + IoSenderManage.updataLockSetCommand( + lockID: BlueManage().connectDeviceName, + userID: await Storage.getUid(), + token: token, + needAuthor: 1, + signKey: signKeyDataList, + privateKey: getPrivateKeyList); + } catch (e) { + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + cancelBlueConnetctToastTimer(); + showToast('上传设置失败:${e.toString()}'.tr); + state.sureBtnState.value = 0; + } } // 上传数据获取锁设置解析 Future _replyUpdataLockSetReply(Reply reply) async { final int status = reply.data[2]; - dismissEasyLoading(); // 关闭loading + // 保持loading状态直到整个过程完成 cancelBlueConnetctToastTimer(); + switch (status) { case 0x00: await _lockDataUpload( uploadType: 1, recordType: 0, records: reply.data.sublist(7, reply.data.length)); - break; + case 0x06: - //无权限 - final List token = reply.data.sublist(3, 7); - final List saveStrList = changeIntListToStringList(token); - Storage.setStringList(saveBlueToken, saveStrList); - - _uploadLockSet(token); + //无权限,尝试重新获取token + try { + final List token = reply.data.sublist(3, 7); + final List saveStrList = changeIntListToStringList(token); + await Storage.setStringList(saveBlueToken, saveStrList); + _uploadLockSet(token); + } catch (e) { + if (EasyLoading.isShow) { + dismissEasyLoading(); // 错误时关闭loading + } + showToast('获取设置权限失败:${e.toString()}'.tr); + state.sureBtnState.value = 0; // 确保重置状态 + } break; + default: - dismissEasyLoading(); - cancelBlueConnetctToastTimer(); + if (EasyLoading.isShow) { + dismissEasyLoading(); // 错误时关闭loading + } + showToast('获取锁设置失败 (错误码: $status)'.tr); + state.sureBtnState.value = 0; // 确保重置状态 break; } } @@ -445,25 +585,43 @@ class ConfiguringWifiLogic extends BaseGetXController { {required int uploadType, required int recordType, required List records}) async { - final LoginEntity entity = await ApiRepository.to.lockDataUpload( - lockId: state.lockBasicInfo.value.lockId ?? -1, - uploadType: uploadType, - recordType: recordType, - records: records, - isUnShowLoading: true); - if (entity.errorCode!.codeIsSuccessful) { - showToast('配网成功'.tr, something: () { - state.isLoading.value = false; - if (state.pageName.value == 'lockSet') { - Get.close(2); - } else { - Get.offAllNamed(Routers.starLockMain); - } + try { + final LoginEntity entity = await ApiRepository.to.lockDataUpload( + lockId: state.lockBasicInfo.value.lockId ?? -1, + uploadType: uploadType, + recordType: recordType, + records: records, + isUnShowLoading: true); - eventBus - .fire(PassCurrentLockInformationEvent(state.lockSetInfoData.value)); - eventBus.fire(SuccessfulDistributionNetwork()); - }); + if (entity.errorCode!.codeIsSuccessful) { + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + showToast('配网成功'.tr, something: () { + state.sureBtnState.value = 0; // 确保重置状态 + if (state.pageName.value == 'lockSet') { + Get.close(2); + } else { + Get.offAllNamed(Routers.starLockMain); + } + + eventBus.fire( + PassCurrentLockInformationEvent(state.lockSetInfoData.value)); + eventBus.fire(SuccessfulDistributionNetwork()); + }); + } else { + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + showToast('数据上传失败:${entity.errorCode}'.tr); + state.sureBtnState.value = 0; // 确保重置状态 + } + } catch (e) { + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + showToast('数据上传异常:${e.toString()}'.tr); + state.sureBtnState.value = 0; // 确保重置状态 } } } diff --git a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_page.dart b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_page.dart index ed653308..51c45a9d 100755 --- a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_page.dart +++ b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_page.dart @@ -25,6 +25,9 @@ class _ConfiguringWifiPageState extends State final ConfiguringWifiLogic logic = Get.put(ConfiguringWifiLogic()); final ConfiguringWifiState state = Get.find().state; + // 添加密码可见性控制 + final RxBool _obscureText = true.obs; + @override Widget build(BuildContext context) { return Scaffold( @@ -39,19 +42,36 @@ class _ConfiguringWifiPageState extends State 'WiFi名称'.tr, '请输入WiFi名字'.tr, state.wifiNameController), Container( width: 1.sw, height: 1.h, color: AppColors.mainBackgroundColor), - configuringWifiTFWidget( + configuringWifiPasswordTFWidget( 'WiFi密码'.tr, '请输入WiFi密码'.tr, state.wifiPWDController), SizedBox( height: 50.h, ), Obx( () => SubmitBtn( - btnName: '确定'.tr, - isDisabled: state.isLoading.isFalse, - onClick: state.isLoading.isTrue + btnName: state.sureBtnState.value == 1 ? '配置中...'.tr : '确定'.tr, + // 当sureBtnState为1时按钮不可用 + isDisabled: state.sureBtnState.value == 1, + onClick: state.sureBtnState.value == 1 ? null : () { FocusScope.of(context).requestFocus(FocusNode()); + // 验证输入 + if (state.wifiNameController.text.isEmpty) { + logic.showToast('请输入WiFi名称'.tr); + return; + } + if (state.wifiPWDController.text.isEmpty) { + logic.showToast('请输入WiFi密码'.tr); + return; + } + // 检查WiFi名称是否包含5G关键字 + if (state.wifiNameController.text + .toLowerCase() + .contains('5g')) { + logic.showToast('请确保使用2.4GHz WiFi网络'.tr); + return; + } logic.senderConfiguringWifiAction(); }, ), @@ -86,7 +106,22 @@ class _ConfiguringWifiPageState extends State ); } - // 接受者信息输入框 + Widget configuringWifiPasswordTFWidget( + String titleStr, String rightTitle, TextEditingController controller) { + return Column( + children: [ + Container(height: 10.h), + CommonItem( + leftTitel: titleStr, + rightTitle: '', + isHaveRightWidget: true, + rightWidget: getPasswordTFWidget(rightTitle, controller)), + Container(height: 10.h), + ], + ); + } + + // 普通输入框 Widget getTFWidget(String tfStr, TextEditingController controller) { return Container( height: 65.h, @@ -95,18 +130,14 @@ class _ConfiguringWifiPageState extends State children: [ Expanded( child: TextField( - //输入框一行 maxLines: 1, inputFormatters: [ FilteringTextInputFormatter.deny('\n'), - // LengthLimitingTextInputFormatter(30), ], controller: controller, autofocus: false, textAlign: TextAlign.end, decoration: InputDecoration( - //输入里面输入文字内边距设置 - // contentPadding: const EdgeInsets.only(top: 12.0, bottom: 8.0), hintText: tfStr, hintStyle: TextStyle(fontSize: 22.sp), focusedBorder: const OutlineInputBorder( @@ -135,6 +166,61 @@ class _ConfiguringWifiPageState extends State ); } + // 密码输入框 + Widget getPasswordTFWidget(String tfStr, TextEditingController controller) { + return Container( + height: 65.h, + width: 300.w, + child: Row( + children: [ + Expanded( + child: Obx( + () => TextField( + maxLines: 1, + obscureText: _obscureText.value, + inputFormatters: [ + FilteringTextInputFormatter.deny('\n'), + ], + controller: controller, + autofocus: false, + textAlign: TextAlign.end, + decoration: InputDecoration( + hintText: tfStr, + hintStyle: TextStyle(fontSize: 22.sp), + focusedBorder: const OutlineInputBorder( + borderSide: + BorderSide(width: 0, color: Colors.transparent)), + disabledBorder: const OutlineInputBorder( + borderSide: + BorderSide(width: 0, color: Colors.transparent)), + enabledBorder: const OutlineInputBorder( + borderSide: + BorderSide(width: 0, color: Colors.transparent)), + border: const OutlineInputBorder( + borderSide: + BorderSide(width: 0, color: Colors.transparent)), + contentPadding: const EdgeInsets.symmetric(vertical: 0), + ), + style: TextStyle( + fontSize: 22.sp, textBaseline: TextBaseline.alphabetic), + ), + ), + ), + IconButton( + icon: Icon( + _obscureText.value ? Icons.visibility_off : Icons.visibility, + color: Colors.grey, + size: 24.sp, + ), + onPressed: () { + _obscureText.value = !_obscureText.value; + }, + ), + ], + ), + ); + } + @override void didChangeDependencies() { super.didChangeDependencies(); diff --git a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_state.dart b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_state.dart index 513fede2..350a2863 100755 --- a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_state.dart +++ b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_state.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:network_info_plus/network_info_plus.dart'; @@ -33,5 +31,4 @@ class ConfiguringWifiState { String getGatewayConfigurationStr = ''; RxBool isLoading = false.obs; - Timer? loadingTimer; } diff --git a/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_logic.dart b/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_logic.dart index 8f95204e..6ee5bea6 100755 --- a/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_logic.dart +++ b/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_logic.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:get/get.dart'; +import 'package:star_lock/app_settings/app_settings.dart'; import 'package:star_lock/blue/io_gateway/io_gateway_getWifiList.dart'; import 'package:star_lock/blue/io_protocol/io_getWifiList.dart'; import 'package:star_lock/talk/starChart/star_chart_manage.dart'; @@ -20,7 +21,9 @@ class WifiListLogic extends BaseGetXController { // 获取解析后的数据 late StreamSubscription _replySubscription; + Timer? _connectionTimer; + /// 初始化订阅,监听设备响应 void _initReplySubscription() { _replySubscription = EventBusManager().eventBus!.on().listen((Reply reply) { @@ -31,78 +34,130 @@ class WifiListLogic extends BaseGetXController { if (reply is GatewayGetWifiListReply) { _replyGetWifiListParameters(reply); } + }, onError: (error) { + // 处理CRC校验失败等错误 + AppLog.log('WiFi列表获取过程中发生错误: $error'); + + // 取消loading状态,显示错误提示 + dismissEasyLoading(); + cancelBlueConnetctToastTimer(); + + // 重置按钮状态,允许重新扫描 + state.sureBtnState.value = 0; + + // 如果是CRC校验失败,显示特定提示 + if (error.toString().contains('CRC')) { + showToast('数据校验失败,请重新扫描'.tr); + } else { + showToast('扫描WiFi失败,请重试'.tr); + } }); } - // 发送获取wifi列表数据解析 + /// 发送获取wifi列表数据解析 Future _replySendGetWifiParameters(Reply reply) async { final int status = reply.data[2]; switch (status) { case 0x00: - //成功 - showEasyLoading(); + //成功 - 不显示loading框,UI中已经有进度指示器 cancelBlueConnetctToastTimer(); - Future.delayed(5.seconds, dismissEasyLoading); break; case 0x06: // 需要鉴权 + dismissEasyLoading(); + AppLog.log('需要设备鉴权,请重试'.tr); + state.sureBtnState.value = 0; break; default: + // 处理其他错误状态 + dismissEasyLoading(); + AppLog.log('获取WiFi列表失败,错误码:$status'.tr); + state.sureBtnState.value = 0; break; } } - // 获取WiFi数据解析 + /// 获取WiFi数据解析 Future _replyGetWifiListParameters(Reply reply) async { final int status = reply.data[2]; switch (status) { case 0x00: //成功 - // showEasyLoading(); dismissEasyLoading(); state.sureBtnState.value = 0; if (reply.data[3] > 0) { reply.data.removeRange(0, 4); - // 把得到的数据按33位分割成数组 然后塞进一个新的数组里面 + // 把得到的数据按33位分割成数组然后处理 final List> getList = splitList(reply.data, 33); final List> uploadList = >[]; + for (int i = 0; i < getList.length; i++) { final List indexList = getList[i]; final Map indexMap = {}; final List wifiName = indexList.sublist(0, 32); - indexMap['wifiName'] = utf8String(wifiName); + final String wifiNameStr = utf8String(wifiName).trim(); + + // 过滤掉空的WiFi名称 + if (wifiNameStr.isEmpty) { + continue; + } + + indexMap['wifiName'] = wifiNameStr; indexMap['rssi'] = (indexList.last - 255).toString(); uploadList.add(indexMap); - state.wifiNameDataList.value = uploadList; } + + // 按信号强度排序WiFi列表 (从强到弱) + uploadList.sort((a, b) => + int.parse(b['rssi']!).compareTo(int.parse(a['rssi']!))); + + state.wifiNameDataList.value = uploadList; + + if (uploadList.isEmpty) { + showToast('未检测到可用的WiFi网络'.tr); + } + } else { + // 处理WiFi列表为空的情况 + state.wifiNameDataList.clear(); + showToast('未检测到可用的WiFi网络'.tr); } break; default: + // 处理其他错误状态 + dismissEasyLoading(); + showToast('解析WiFi列表失败,错误码:$status'.tr); + state.sureBtnState.value = 0; break; } } - // 获取wifi列表 + /// 获取WiFi列表 Future senderGetWifiListWifiAction() async { if (state.sureBtnState.value == 1) { return; } state.sureBtnState.value = 1; + state.wifiNameDataList.clear(); // 清空之前的列表 - showEasyLoading(); + // 不显示loading框,UI中已经有进度指示器 showBlueConnetctToastTimer(action: () { - dismissEasyLoading(); state.sureBtnState.value = 0; }); + BlueManage().blueSendData(BlueManage().connectDeviceName, (BluetoothConnectionState connectionState) async { if (connectionState == BluetoothConnectionState.connected) { - IoSenderManage.gatewayGetWifiCommand( - userID: await Storage.getUid(), - ); + try { + IoSenderManage.gatewayGetWifiCommand( + userID: await Storage.getUid(), + ); + } catch (e) { + state.sureBtnState.value = 0; + cancelBlueConnetctToastTimer(); + showToast('发送获取WiFi列表请求失败:${e.toString()}'.tr); + } } else if (connectionState == BluetoothConnectionState.disconnected) { - dismissEasyLoading(); state.sureBtnState.value = 0; cancelBlueConnetctToastTimer(); if (state.ifCurrentScreen.value == true) { @@ -113,22 +168,26 @@ class WifiListLogic extends BaseGetXController { } @override - void onReady() { + void onReady() async { super.onReady(); - _initReplySubscription(); + await senderGetWifiListWifiAction(); } @override void onInit() { super.onInit(); - - senderGetWifiListWifiAction(); + // 页面进入时标记为当前页面 + state.ifCurrentScreen.value = true; } @override void onClose() { - super.onClose(); + // 取消所有计时器和订阅,防止内存泄漏 _replySubscription.cancel(); + _connectionTimer?.cancel(); + cancelBlueConnetctToastTimer(); + state.ifCurrentScreen.value = false; + super.onClose(); } } diff --git a/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_page.dart b/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_page.dart index 2508cc27..fd4c6765 100755 --- a/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_page.dart +++ b/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_page.dart @@ -23,6 +23,32 @@ class _WifiListPageState extends State { final WifiListLogic logic = Get.put(WifiListLogic()); final WifiListState state = Get.find().state; + /// 计算WiFi信号强度图标 + IconData _getWifiSignalIcon(String rssi) { + final int rssiValue = int.parse(rssi); + if (rssiValue >= -50) { + return Icons.signal_wifi_4_bar; + } else if (rssiValue >= -70) { + return Icons.network_wifi; + } else if (rssiValue >= -80) { + return Icons.network_wifi_2_bar_rounded; + } else { + return Icons.signal_wifi_0_bar; + } + } + + @override + void initState() { + super.initState(); + state.ifCurrentScreen.value = true; + } + + @override + void dispose() { + state.ifCurrentScreen.value = false; + super.dispose(); + } + @override Widget build(BuildContext context) { return WillPopScope( @@ -38,13 +64,15 @@ class _WifiListPageState extends State { barTitle: 'WIFI列表'.tr, haveBack: state.pageName.value == 'lockSet', actionsList: [ - TextButton( - child: Text( - '刷新'.tr, - style: TextStyle(color: Colors.white, fontSize: 24.sp), - ), - onPressed: logic.senderGetWifiListWifiAction, - ), + Obx(() => TextButton( + child: Text( + '刷新'.tr, + style: TextStyle(color: Colors.white, fontSize: 24.sp), + ), + onPressed: state.sureBtnState.value == 0 + ? logic.senderGetWifiListWifiAction + : null, + )), ], backgroundColor: AppColors.mainColor, ), @@ -52,26 +80,56 @@ class _WifiListPageState extends State { children: [ Expanded( child: Obx(() => state.wifiNameDataList.value.isNotEmpty - ? ListView.builder( - itemCount: state.wifiNameDataList.value.length, - itemBuilder: (BuildContext c, int index) { - Map wifiNameStr = state.wifiNameDataList.value[index]; - return _messageListItem( - wifiNameStr['wifiName'], wifiNameStr['rssi'], () { - Get.toNamed(Routers.configuringWifiPage, - arguments: { - 'lockSetInfoData': - state.lockSetInfoData.value, - 'wifiName': wifiNameStr['wifiName'], - 'pageName': state.pageName.value, - }); - }); - }) - : NoData( - noDataHeight: 1.sh - - ScreenUtil().statusBarHeight - - ScreenUtil().bottomBarHeight - - 64.h)), + ? RefreshIndicator( + onRefresh: () async { + if (state.sureBtnState.value == 0) { + await logic.senderGetWifiListWifiAction(); + } + }, + child: ListView.builder( + itemCount: state.wifiNameDataList.value.length, + itemBuilder: (BuildContext c, int index) { + Map wifiNameStr = + state.wifiNameDataList.value[index]; + return _messageListItem( + wifiNameStr['wifiName'], wifiNameStr['rssi'], + () { + Get.toNamed(Routers.configuringWifiPage, + arguments: { + 'lockSetInfoData': + state.lockSetInfoData.value, + 'wifiName': wifiNameStr['wifiName'], + 'pageName': state.pageName.value, + }); + }); + }), + ) + : Obx(() => state.sureBtnState.value == 1 + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 50.w, + height: 50.w, + child: CircularProgressIndicator( + strokeWidth: 4.w, + valueColor: AlwaysStoppedAnimation( + AppColors.mainColor, + ), + backgroundColor: Colors.grey[200], + ), + ), + SizedBox(height: 20.h), + Text('正在扫描WiFi网络...\n请确保设备处于正常状态'.tr, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24.sp, + color: AppColors.blackColor)) + ], + ), + ) + : NoData())), ), state.pageName.value == 'saveLock' ? SubmitBtn( @@ -140,24 +198,37 @@ class _WifiListPageState extends State { height: 79.h, width: 1.sw - 20.w * 2, child: Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( + flex: 4, child: Text( - '$wifiName(${rssi}db)', + wifiName, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( - fontSize: 22.sp, color: AppColors.blackColor), + fontSize: 24.sp, color: AppColors.blackColor), ), ), - // Text( - // rssi, - // maxLines: 1, - // overflow: TextOverflow.ellipsis, - // style: TextStyle( - // fontSize: 22.sp, color: AppColors.blackColor), - // ) + Flexible( + flex: 1, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Icon( + _getWifiSignalIcon(rssi), + color: AppColors.mainColor, + size: 24.sp, + ), + SizedBox(width: 8.w), + Text( + '$rssi dB', + style: + TextStyle(fontSize: 18.sp, color: Colors.black), + ), + ], + ), + ) ], ), ), From 3a262d80dddf606037c72d45a6662407a7b6c497 Mon Sep 17 00:00:00 2001 From: liyi Date: Sat, 26 Apr 2025 15:15:46 +0800 Subject: [PATCH 004/151] =?UTF-8?q?fix:=E6=B3=A8=E9=87=8ACRC=E9=AA=8C?= =?UTF-8?q?=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/blue/reciver_data.dart | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/blue/reciver_data.dart b/lib/blue/reciver_data.dart index 2fc7fe54..0b0f4c61 100755 --- a/lib/blue/reciver_data.dart +++ b/lib/blue/reciver_data.dart @@ -61,16 +61,16 @@ class CommandReciverManager { final int dataSize = data.length; // 验证CRC校验 - if (dataSize >= 2) { - final int calculatedCrc = - _calculateCRC16(data.sublist(0, dataSize - 2), dataSize - 2); - final int receivedCrc = (data[dataSize - 2] << 8) | data[dataSize - 1]; - - if (calculatedCrc != receivedCrc) { - AppLog.log('CRC校验失败'); - return; - } - } + // if (dataSize >= 2) { + // final int calculatedCrc = + // _calculateCRC16(data.sublist(0, dataSize - 2), dataSize - 2); + // final int receivedCrc = (data[dataSize - 2] << 8) | data[dataSize - 1]; + // + // if (calculatedCrc != receivedCrc) { + // throw Exception('CRC校验失败'); + // return; + // } + // } // 当小于包头加起来13个字节 if (dataSize < 13) { return; From 6a848ac23e1d95690294074ede25b99a690c3ade Mon Sep 17 00:00:00 2001 From: liyi Date: Sat, 26 Apr 2025 15:16:02 +0800 Subject: [PATCH 005/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E7=A6=81?= =?UTF-8?q?=E7=94=A8=E6=8C=89=E9=92=AE=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/tools/submitBtn.dart | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/tools/submitBtn.dart b/lib/tools/submitBtn.dart index 01511077..81a57c62 100755 --- a/lib/tools/submitBtn.dart +++ b/lib/tools/submitBtn.dart @@ -53,15 +53,11 @@ class SubmitBtn extends StatelessWidget { ), child: ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: isDisabled == false + backgroundColor: isDisabled == true ? AppColors.btnDisableColor : (isDelete == true ? Colors.red : AppColors.mainColor), ), - onPressed: () { - if (onClick != null) { - onClick!(); - } - }, + onPressed: isDisabled == true ? null : onClick, child: Text( btnName!, textAlign: TextAlign.center, // 文本居中对齐 From de71435ba15504736f3e53d83063cf277c472b19 Mon Sep 17 00:00:00 2001 From: liyi Date: Sat, 26 Apr 2025 15:16:39 +0800 Subject: [PATCH 006/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E8=93=9D?= =?UTF-8?q?=E7=89=99=E5=86=99=E5=85=A5=E9=80=BB=E8=BE=91=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E9=87=8D=E8=AF=95=E6=9C=BA=E5=88=B6=EF=BC=9B=E5=A4=84?= =?UTF-8?q?=E7=90=86GATT=E9=94=99=E8=AF=AF133?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/blue/blue_manage.dart | 60 ++++++++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/lib/blue/blue_manage.dart b/lib/blue/blue_manage.dart index 359758b9..70d9bc45 100755 --- a/lib/blue/blue_manage.dart +++ b/lib/blue/blue_manage.dart @@ -775,7 +775,7 @@ class BlueManage { } } - // 写入 + /// 写入蓝牙特征值,并等待响应 Future writeCharacteristicWithResponse(List value) async { final List services = await bluetoothConnectDevice!.discoverServices(); @@ -785,30 +785,64 @@ class BlueManage { in service.characteristics) { if (characteristic.characteristicUuid == _characteristicIdWrite) { try { + // 添加重试机制 + int retryCount = 0; + const int maxRetries = 3; + const int retryDelayMs = 500; + final List valueList = value; final List subData = splitList(valueList, _mtuSize!); - // AppLog.log('writeCharacteristicWithResponse _mtuSize:$_mtuSize 得到的分割数据:$subData'); + for (int i = 0; i < subData.length; i++) { - if (characteristic.properties.writeWithoutResponse) { - // 使用WRITE_NO_RESPONSE属性写入值 - await characteristic.write(subData[i], withoutResponse: true); - } else if (characteristic.properties.write) { - // 使用WRITE属性写入值 - await characteristic.write(subData[i]); - } else { - // 特性不支持写入 - throw Exception( - 'This characteristic does not support writing.'); + // 对每个数据包都应用重试逻辑 + bool packetSent = false; + retryCount = 0; + + while (!packetSent && retryCount < maxRetries) { + try { + if (characteristic.properties.writeWithoutResponse) { + await characteristic.write(subData[i], withoutResponse: true); + } else if (characteristic.properties.write) { + await characteristic.write(subData[i]); + } else { + // 特性不支持写入 + throw Exception('This characteristic does not support writing.'); + } + + // 如果到这里没有异常,则包发送成功 + packetSent = true; + } catch (e) { + if (e.toString().contains('UNKNOWN_GATT_ERROR (133)') && retryCount < maxRetries - 1) { + // GATT错误133,尝试重试 + retryCount++; + AppLog.log('蓝牙写入失败(GATT 133),数据包 ${i+1}/${subData.length} 正在重试 $retryCount/$maxRetries...'); + await Future.delayed(Duration(milliseconds: retryDelayMs)); + continue; + } else { + // 其他错误或已达到最大重试次数,抛出异常 + AppLog.log('APP写入失败: $e'); + throw e; + } + } + } + + if (!packetSent) { + throw Exception('蓝牙写入失败,数据包 ${i+1}/${subData.length} 已达到最大重试次数'); } } + + return; // 所有数据包都发送成功 } on Exception catch (e, s) { - AppLog.log('APP写入失败: $e $s'); + AppLog.log('APP写入失败: $e $s'); rethrow; } } } } } + + // 如果找不到合适的特性用于写入 + throw Exception('未找到适合写入的蓝牙特性'); } // 停止扫描蓝牙设备 From 9d8f0051277c880269062b6f448ed824f10c537b Mon Sep 17 00:00:00 2001 From: liyi Date: Sat, 26 Apr 2025 15:51:16 +0800 Subject: [PATCH 007/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E9=85=8D?= =?UTF-8?q?=E7=BD=91=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuringWifi_logic.dart | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart index a940ca4e..3b23c309 100755 --- a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart +++ b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart @@ -55,7 +55,7 @@ class ConfiguringWifiLogic extends BaseGetXController { } /// 更新网络信息到服务器 - void updateNetworkInfo({ + Future updateNetworkInfo({ required String peerId, required String wifiName, required String secretKey, @@ -71,27 +71,12 @@ class ConfiguringWifiLogic extends BaseGetXController { secretKey: secretKey, peerId: peerId, ); - - if (entity.errorCode!.codeIsSuccessful) { - // 设置锁的peerID - StartChartManage().lockNetworkInfo = DeviceNetworkInfo( - wifiName: wifiName, - networkMac: networkMac, - secretKey: secretKey, - peerId: peerId, - ); - - await _getUploadLockSet(); - } else { - dismissEasyLoading(); - showToast('网络配置失败,请重试'.tr); - state.sureBtnState.value = 0; - } + return entity; } catch (e) { dismissEasyLoading(); - showToast('网络配置异常:${e.toString()}'.tr); state.sureBtnState.value = 0; AppLog.log('网络配置异常:$e'); + return LoginEntity(); } } @@ -174,19 +159,34 @@ class ConfiguringWifiLogic extends BaseGetXController { throw Exception('Missing required network information'); } - /// 配网成功后,赋值锁的peerId - StartChartManage().lockPeerId = peerId; - - // 保存到缓存 - await Storage.saveLockNetWorkInfo(jsonMap); - // 上报服务器 - 注意: sureBtnState 状态将在 updateNetworkInfo 方法中或其回调中完成重置 - updateNetworkInfo( + final info = await updateNetworkInfo( peerId: peerId, wifiName: wifiName ?? '', secretKey: secretKey, deviceMac: deviceMac ?? '', networkMac: networkMac ?? ''); + if (info.errorCode!.codeIsSuccessful) { + // 设置锁的peerID + StartChartManage().lockNetworkInfo = DeviceNetworkInfo( + wifiName: wifiName, + networkMac: networkMac, + secretKey: secretKey, + peerId: peerId, + ); + + /// 配网成功后,赋值锁的peerId + StartChartManage().lockPeerId = peerId; + + // 保存到缓存 + await Storage.saveLockNetWorkInfo(jsonMap); + + await _getUploadLockSet(); + } else { + dismissEasyLoading(); + showToast('网络配置失败,请重试'.tr); + state.sureBtnState.value = 0; + } } catch (e) { if (EasyLoading.isShow) { dismissEasyLoading(); @@ -495,13 +495,13 @@ class ConfiguringWifiLogic extends BaseGetXController { throw Exception('Token is empty'); } final List getTokenList = changeStringListToIntList(token); + // 蓝牙获取锁设置 await _uploadLockSet(getTokenList); } catch (e) { if (EasyLoading.isShow) { dismissEasyLoading(); } cancelBlueConnetctToastTimer(); - showToast('获取设置失败:${e.toString()}'.tr); state.sureBtnState.value = 0; } } @@ -613,14 +613,14 @@ class ConfiguringWifiLogic extends BaseGetXController { if (EasyLoading.isShow) { dismissEasyLoading(); } - showToast('数据上传失败:${entity.errorCode}'.tr); + // showToast('数据上传失败:${entity.errorCode}'.tr); state.sureBtnState.value = 0; // 确保重置状态 } } catch (e) { if (EasyLoading.isShow) { dismissEasyLoading(); } - showToast('数据上传异常:${e.toString()}'.tr); + // showToast('数据上传异常:${e.toString()}'.tr); state.sureBtnState.value = 0; // 确保重置状态 } } From 3e2009069a03777a7ae9cea1c4ee5f577980f819 Mon Sep 17 00:00:00 2001 From: liyi Date: Sun, 27 Apr 2025 09:22:18 +0800 Subject: [PATCH 008/151] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E8=BF=9B?= =?UTF-8?q?=E5=85=A5=E6=8C=87=E7=BA=B9=E4=B8=8D=E8=87=AA=E5=8A=A8=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E6=8C=87=E7=BA=B9=E5=88=97=E8=A1=A8=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/main/lockDetail/card/cardList/cardList_logic.dart | 3 ++- lib/main/lockDetail/face/faceList/faceList_logic.dart | 3 ++- .../fingerprint/fingerprintList/fingerprintList_logic.dart | 1 + lib/main/lockDetail/palm/palmList/palmList_logic.dart | 1 + 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/main/lockDetail/card/cardList/cardList_logic.dart b/lib/main/lockDetail/card/cardList/cardList_logic.dart index 2fc2e376..c007eca1 100755 --- a/lib/main/lockDetail/card/cardList/cardList_logic.dart +++ b/lib/main/lockDetail/card/cardList/cardList_logic.dart @@ -249,8 +249,9 @@ class CardListLogic extends BaseGetXController { _initReplySubscription(); // _initRefreshAction(); + await getICCardListData(isRefresh: true); } - await getICCardListData(isRefresh: true); + } @override diff --git a/lib/main/lockDetail/face/faceList/faceList_logic.dart b/lib/main/lockDetail/face/faceList/faceList_logic.dart index a1499556..2f6e8a18 100755 --- a/lib/main/lockDetail/face/faceList/faceList_logic.dart +++ b/lib/main/lockDetail/face/faceList/faceList_logic.dart @@ -443,8 +443,9 @@ class FaceListLogic extends BaseGetXController { // senderCheckingCardStatus(); // senderCheckingUserInfoCount(); + await getFaceListData(isRefresh: true); } - getFaceListData(isRefresh: true); + } @override diff --git a/lib/main/lockDetail/fingerprint/fingerprintList/fingerprintList_logic.dart b/lib/main/lockDetail/fingerprint/fingerprintList/fingerprintList_logic.dart index d65269cd..dc5bb895 100755 --- a/lib/main/lockDetail/fingerprint/fingerprintList/fingerprintList_logic.dart +++ b/lib/main/lockDetail/fingerprint/fingerprintList/fingerprintList_logic.dart @@ -461,6 +461,7 @@ class FingerprintListLogic extends BaseGetXController { _initReplySubscription(); // _initRefreshAction(); + await getFingerprintsListData(isRefresh: true); } } diff --git a/lib/main/lockDetail/palm/palmList/palmList_logic.dart b/lib/main/lockDetail/palm/palmList/palmList_logic.dart index 8d15fa33..c4b94410 100755 --- a/lib/main/lockDetail/palm/palmList/palmList_logic.dart +++ b/lib/main/lockDetail/palm/palmList/palmList_logic.dart @@ -234,6 +234,7 @@ class PalmListLogic extends BaseGetXController { _initReplySubscription(); // _initRefreshAction(); + await getPalmListData(isRefresh: true); } } From 25fb142765379652f4d5ff9e02f2a8048d0a05c6 Mon Sep 17 00:00:00 2001 From: liyi Date: Sun, 27 Apr 2025 09:53:05 +0800 Subject: [PATCH 009/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E6=97=A0?= =?UTF-8?q?=E9=9F=B3=E9=A2=91=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handle/impl/udp_talk_accept_handler.dart | 1 + .../handle/impl/udp_talk_request_handler.dart | 51 +++++----- .../views/talkView/talk_view_logic.dart | 31 ++++-- .../starChart/webView/h264_web_logic.dart | 94 ++++++++++++++----- lib/talk/starChart/webView/h264_web_view.dart | 7 +- 5 files changed, 127 insertions(+), 57 deletions(-) diff --git a/lib/talk/starChart/handle/impl/udp_talk_accept_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_accept_handler.dart index 2605525c..baaae2e8 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_accept_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_accept_handler.dart @@ -78,6 +78,7 @@ class UdpTalkAcceptHandler extends ScpMessageBaseHandle } } + /// 收到同意接听回复之后增加音频的期望数据 void _handleSendExpect() { final LockListInfoItemEntity currentKeyInfo = CommonDataManage().currentKeyInfo; diff --git a/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart index 974c8f25..91fdcc83 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart @@ -1,20 +1,15 @@ import 'dart:convert'; import 'dart:io'; -import 'package:flutter/services.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/app_settings/app_settings.dart'; import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart'; import 'package:star_lock/talk/starChart/constant/message_type_constant.dart'; -import 'package:star_lock/talk/starChart/constant/talk_constant.dart'; import 'package:star_lock/talk/starChart/constant/talk_status.dart'; import 'package:star_lock/talk/starChart/entity/scp_message.dart'; import 'package:star_lock/talk/starChart/handle/scp_message_base_handle.dart'; import 'package:star_lock/talk/starChart/handle/scp_message_handle.dart'; -import 'package:star_lock/talk/starChart/proto/gateway_reset.pb.dart'; import 'package:star_lock/talk/starChart/proto/generic.pb.dart'; import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart'; import 'package:star_lock/talk/starChart/proto/talk_request.pb.dart'; @@ -28,26 +23,10 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle RxString currentLanguage = CurrentLocaleTool.getCurrentLocaleString().obs; // 当前选择语言 - // 添加上次处理请求的时间戳 - int _lastRequestTime = 0; - @override void handleReq(ScpMessage scpMessage) async { - final currentTime = DateTime.now().millisecondsSinceEpoch; - // 确保与上次请求间隔至少1秒 - if (currentTime - _lastRequestTime < 1000) { - // 如果间隔小于1秒,直接拒绝请求 - replyErrorMessage(scpMessage); - AppLog.log('对讲请求过于频繁,已拒绝'); - return; - } - - // 更新最后处理时间 - _lastRequestTime = currentTime; - // 判断是否登录账户 final loginData = await Storage.getLoginData(); - // 如果登录账户不为空,且不是被动接听状态,且不是接听成功状态 if (loginData != null && (talkStatus.status != TalkStatus.passiveCallWaitingAnswer || @@ -77,6 +56,8 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle // 收到对讲请求的应答 startChartManage.FromPeerId = scpMessage.ToPeerId!; startChartManage.ToPeerId = scpMessage.FromPeerId!; + // 处理预期数据格式 + _handleResponseSendExpect(); // 发送预期数据 startChartManage.startTalkExpectTimer(); // 停止发送对讲请求 @@ -99,7 +80,7 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle // 来电事件的处理 void _talkRequestEvent({required String talkObjectName}) { // 发送预期数据、通知锁板需要获取视频数据 - _handleSendExpect(); + _handleRequestSendExpect(); // 播放铃声 //test:使用自定义铃声 playRingtone(); @@ -188,7 +169,8 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle } } - void _handleSendExpect() { + /// app收到的对讲请求后,发送的预期数据 + void _handleRequestSendExpect() { final LockListInfoItemEntity currentKeyInfo = CommonDataManage().currentKeyInfo; final isH264 = currentKeyInfo.lockFeature?.isH264 == 1; @@ -209,4 +191,27 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle print('锁不支持H264和MJPEG,默认发送图像视频格式期望数据'); } } + + /// app主动发请求,收到回复后发送的预期数据 + void _handleResponseSendExpect() { + final LockListInfoItemEntity currentKeyInfo = + CommonDataManage().currentKeyInfo; + final isH264 = currentKeyInfo.lockFeature?.isH264 == 1; + final isMJpeg = currentKeyInfo.lockFeature?.isMJpeg == 1; + + // 优先使用H264,其次是MJPEG + if (isH264) { + // 锁支持H264,发送H264视频和G711音频期望 + startChartManage.sendH264VideoAndG711AudioTalkExpectData(); + print('锁支持H264,发送H264视频格式期望数据'); + } else if (isMJpeg) { + // 锁只支持MJPEG,发送图像视频和G711音频期望 + startChartManage.sendImageVideoAndG711AudioTalkExpectData(); + print('锁不支持H264,支持MJPEG,发送MJPEG视频格式期望数据'); + } else { + // 默认使用图像视频 + startChartManage.sendImageVideoAndG711AudioTalkExpectData(); + print('锁不支持H264和MJPEG,默认发送图像视频格式期望数据'); + } + } } diff --git a/lib/talk/starChart/views/talkView/talk_view_logic.dart b/lib/talk/starChart/views/talkView/talk_view_logic.dart index eaf051c1..cc959c0b 100644 --- a/lib/talk/starChart/views/talkView/talk_view_logic.dart +++ b/lib/talk/starChart/views/talkView/talk_view_logic.dart @@ -51,7 +51,6 @@ class TalkViewLogic extends BaseGetXController { int _startAudioTime = 0; // 开始播放时间戳 bool _isFirstFrame = true; // 是否是第一帧 - // 定义音频帧缓冲和发送函数 final List _bufferedAudioFrames = []; @@ -65,6 +64,10 @@ class TalkViewLogic extends BaseGetXController { int _lastFpsUpdateTime = 0; Timer? _fpsTimer; + // 添加监听状态和订阅引用 + bool _isListening = false; + StreamSubscription? _streamSubscription; + /// 初始化音频播放器 void _initFlutterPcmSound() { const int sampleRate = 8000; @@ -97,7 +100,15 @@ class TalkViewLogic extends BaseGetXController { // 监听音视频数据流 void _startListenTalkData() { - state.talkDataRepository.talkDataStream + // 防止重复监听 + if (_isListening) { + AppLog.log("已经存在数据流监听,避免重复监听"); + return; + } + + AppLog.log("==== 启动新的数据流监听 ===="); + _isListening = true; + _streamSubscription = state.talkDataRepository.talkDataStream .listen((TalkDataModel talkDataModel) async { final talkData = talkDataModel.talkData; final contentType = talkData!.contentType; @@ -106,13 +117,13 @@ class TalkViewLogic extends BaseGetXController { // 判断数据类型,进行分发处理 switch (contentType) { case TalkData_ContentTypeE.G711: - // // 第一帧到达时记录开始时间 - if (_isFirstAudioFrame) { - _startAudioTime = currentTime; - _isFirstAudioFrame = false; - } + // // 第一帧到达时记录开始时间 + if (_isFirstAudioFrame) { + _startAudioTime = currentTime; + _isFirstAudioFrame = false; + } - // 计算音频延迟 + // 计算音频延迟 final expectedTime = _startAudioTime + talkData.durationMs; final audioDelay = currentTime - expectedTime; @@ -384,7 +395,6 @@ class TalkViewLogic extends BaseGetXController { } } - /// 获取权限状态 Future getPermissionStatus() async { final Permission permission = Permission.microphone; @@ -504,6 +514,9 @@ class TalkViewLogic extends BaseGetXController { state.oneMinuteTimeTimer?.cancel(); // 取消旧定时器 state.oneMinuteTimeTimer = null; // 取消旧定时器 state.oneMinuteTime.value = 0; + // 取消数据流监听 + _streamSubscription?.cancel(); + _isListening = false; super.onClose(); } diff --git a/lib/talk/starChart/webView/h264_web_logic.dart b/lib/talk/starChart/webView/h264_web_logic.dart index 4f5789c2..b7307383 100644 --- a/lib/talk/starChart/webView/h264_web_logic.dart +++ b/lib/talk/starChart/webView/h264_web_logic.dart @@ -54,6 +54,10 @@ class H264WebViewLogic extends BaseGetXController { final Queue> _frameBuffer = Queue>(); static const int FRAME_BUFFER_SIZE = 25; + // 添加监听状态和订阅引用 + bool _isListening = false; + StreamSubscription? _streamSubscription; + @override void onInit() { // 初始化 WebView 控制器 @@ -122,7 +126,15 @@ class H264WebViewLogic extends BaseGetXController { } void _createFramesStreamListen() async { - state.talkDataRepository.talkDataStream + // 防止重复监听 + if (_isListening) { + AppLog.log("已经存在数据流监听,避免重复监听"); + return; + } + + AppLog.log("==== 启动新的数据流监听 ===="); + _isListening = true; + _streamSubscription = state.talkDataRepository.talkDataStream .listen((TalkDataModel talkDataModel) async { final talkData = talkDataModel.talkData; final contentType = talkData!.contentType; @@ -131,30 +143,33 @@ class H264WebViewLogic extends BaseGetXController { // 判断数据类型,进行分发处理 switch (contentType) { case TalkData_ContentTypeE.G711: - // // 第一帧到达时记录开始时间 - if (_isFirstAudioFrame) { - _startAudioTime = currentTime; - _isFirstAudioFrame = false; - } - - // 计算音频延迟 - final expectedTime = _startAudioTime + talkData.durationMs; - final audioDelay = currentTime - expectedTime; - - // 如果延迟太大,清空缓冲区并直接播放 - if (audioDelay > 500) { - state.audioBuffer.clear(); - if (state.isOpenVoice.value) { - _playAudioFrames(); + if (state.isShowLoading.isFalse) { + // // 第一帧到达时记录开始时间 + if (_isFirstAudioFrame) { + _startAudioTime = currentTime; + _isFirstAudioFrame = false; } - return; + + // 计算音频延迟 + final expectedTime = _startAudioTime + talkData.durationMs; + final audioDelay = currentTime - expectedTime; + + // 如果延迟太大,清空缓冲区并直接播放 + if (audioDelay > 500) { + state.audioBuffer.clear(); + if (state.isOpenVoice.value) { + _playAudioFrames(); + } + return; + } + if (state.audioBuffer.length >= audioBufferSize) { + state.audioBuffer.removeAt(0); // 丢弃最旧的数据 + } + state.audioBuffer.add(talkData); // 添加新数据 + // 添加音频播放逻辑,与视频类似 + _playAudioFrames(); } - if (state.audioBuffer.length >= audioBufferSize) { - state.audioBuffer.removeAt(0); // 丢弃最旧的数据 - } - state.audioBuffer.add(talkData); // 添加新数据 - // 添加音频播放逻辑,与视频类似 - _playAudioFrames(); + break; case TalkData_ContentTypeE.H264: // // 添加新帧到缓冲区 @@ -537,6 +552,39 @@ class H264WebViewLogic extends BaseGetXController { } } + /// 停止播放音频 + void _stopPlayG711Data() async { + await FlutterPcmSound.pause(); + await FlutterPcmSound.stop(); + await FlutterPcmSound.clear(); + } + + @override + void onClose() { + _stopPlayG711Data(); // 停止播放音频 + + state.audioBuffer.clear(); // 清空音频缓冲区 + + state.oneMinuteTimeTimer?.cancel(); + state.oneMinuteTimeTimer = null; + + // 停止播放音频 + stopProcessingAudio(); + + state.oneMinuteTimeTimer?.cancel(); // 取消旧定时器 + state.oneMinuteTimeTimer = null; // 取消旧定时器 + state.oneMinuteTime.value = 0; + + // 取消数据流监听 + _streamSubscription?.cancel(); + _isListening = false; + + // 重置期望数据 + StartChartManage().reSetDefaultTalkExpect(); + + super.onClose(); + } + @override void dispose() { // _mockDataTimer?.cancel(); diff --git a/lib/talk/starChart/webView/h264_web_view.dart b/lib/talk/starChart/webView/h264_web_view.dart index 51aa30d8..aa18aa3c 100644 --- a/lib/talk/starChart/webView/h264_web_view.dart +++ b/lib/talk/starChart/webView/h264_web_view.dart @@ -7,6 +7,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:star_lock/app_settings/app_colors.dart'; import 'package:star_lock/app_settings/app_settings.dart'; +import 'package:star_lock/talk/call/callTalk.dart'; import 'package:star_lock/talk/starChart/constant/talk_status.dart'; import 'package:star_lock/talk/starChart/handle/other/talk_data_repository.dart'; import 'package:star_lock/talk/starChart/proto/talk_data.pbserver.dart'; @@ -413,8 +414,10 @@ class _H264WebViewState extends State } @override void dispose() { - state.animationController.dispose(); // 确保释放控制器 - super.dispose(); + state.animationController.dispose(); + CallTalk().finishAVData(); + // UdpTalkDataHandler().resetDataRates(); + super.dispose(); } } From 48d4081b3eba41af622bead30efb01729a1dcbeb Mon Sep 17 00:00:00 2001 From: liyi Date: Sun, 27 Apr 2025 10:12:34 +0800 Subject: [PATCH 010/151] =?UTF-8?q?fix:=E9=80=80=E5=87=BA=E5=90=8E?= =?UTF-8?q?=E5=8F=B0=E6=97=B6=E8=87=AA=E5=8A=A8=E6=8C=82=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/talk/starChart/star_chart_manage.dart | 11 ++++++++++- lib/talk/starChart/status/appLifecycle_observer.dart | 3 +-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/talk/starChart/star_chart_manage.dart b/lib/talk/starChart/star_chart_manage.dart index 66de4aeb..1fb79272 100644 --- a/lib/talk/starChart/star_chart_manage.dart +++ b/lib/talk/starChart/star_chart_manage.dart @@ -1199,6 +1199,16 @@ class StartChartManage { /// 销毁资源 void destruction() async { + // 先挂断 + final status = talkStatus.status; + if (status == TalkStatus.passiveCallWaitingAnswer || + status == TalkStatus.proactivelyCallWaitingAnswer || + status == TalkStatus.answeredSuccessfully || + status == TalkStatus.uninitialized) { + startTalkRejectMessageTimer(); + startTalkHangupMessageTimer(); + await Future.delayed(Duration(seconds: 1)); + } isOnlineStarChartServer = false; // 停止发送心跳消息 stopHeartbeat(); @@ -1226,7 +1236,6 @@ class StartChartManage { await Storage.removerStarChartRegisterNodeInfo(); // 关闭udp服务 closeUdpSocket(); - } /// 重置数据 diff --git a/lib/talk/starChart/status/appLifecycle_observer.dart b/lib/talk/starChart/status/appLifecycle_observer.dart index 37b70152..03a356d1 100644 --- a/lib/talk/starChart/status/appLifecycle_observer.dart +++ b/lib/talk/starChart/status/appLifecycle_observer.dart @@ -30,14 +30,13 @@ class AppLifecycleObserver extends WidgetsBindingObserver { // 处理应用程序进入后台的逻辑 final status = StartChartManage().talkStatus.status; - if (status == TalkStatus.passiveCallWaitingAnswer || status == TalkStatus.proactivelyCallWaitingAnswer || status == TalkStatus.answeredSuccessfully || status == TalkStatus.uninitialized) { - StartChartManage().destruction(); Get.back(); } + StartChartManage().destruction(); } void onAppResumed() async { From 3831083f7765d11c1be9dd35417e36b1a380d371 Mon Sep 17 00:00:00 2001 From: liyi Date: Sun, 27 Apr 2025 11:27:31 +0800 Subject: [PATCH 011/151] =?UTF-8?q?fix:=E5=9B=9E=E5=A4=8D=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E5=8E=9F=E6=9C=89=E7=9A=84=E7=A6=81=E7=94=A8=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/tools/submitBtn.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/tools/submitBtn.dart b/lib/tools/submitBtn.dart index 81a57c62..01511077 100755 --- a/lib/tools/submitBtn.dart +++ b/lib/tools/submitBtn.dart @@ -53,11 +53,15 @@ class SubmitBtn extends StatelessWidget { ), child: ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: isDisabled == true + backgroundColor: isDisabled == false ? AppColors.btnDisableColor : (isDelete == true ? Colors.red : AppColors.mainColor), ), - onPressed: isDisabled == true ? null : onClick, + onPressed: () { + if (onClick != null) { + onClick!(); + } + }, child: Text( btnName!, textAlign: TextAlign.center, // 文本居中对齐 From 942a4f365866937a6e1ac6d8afb73b19c793d3f1 Mon Sep 17 00:00:00 2001 From: liyi Date: Mon, 28 Apr 2025 09:20:11 +0800 Subject: [PATCH 012/151] =?UTF-8?q?fix:SKY=E7=8E=AF=E5=A2=83=E4=B8=8B?= =?UTF-8?q?=E6=B3=A8=E9=87=8A=E8=AF=A5=E9=94=81=E5=B7=B2=E8=A2=AB=E9=87=8D?= =?UTF-8?q?=E7=BD=AE=E7=9A=84=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/blue/blue_manage.dart | 42 +++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/lib/blue/blue_manage.dart b/lib/blue/blue_manage.dart index 70d9bc45..064fe984 100755 --- a/lib/blue/blue_manage.dart +++ b/lib/blue/blue_manage.dart @@ -5,6 +5,7 @@ import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:get/get.dart'; import 'package:star_lock/app_settings/app_settings.dart'; +import 'package:star_lock/flavors.dart'; import 'package:star_lock/tools/bugly/bugly_tool.dart'; import 'package:star_lock/tools/commonDataManage.dart'; @@ -551,7 +552,10 @@ class BlueManage { }); } else { connectStateCallBack(BluetoothConnectionState.disconnected); - EasyLoading.showToast('该锁已被重置'.tr, duration: 2000.milliseconds); + if (!F.isSKY) { + EasyLoading.showToast('该锁已被重置'.tr, duration: 2000.milliseconds); + } + scanDevices.clear(); BuglyTool.uploadException( @@ -582,7 +586,9 @@ class BlueManage { }); } else { connectStateCallBack(BluetoothConnectionState.disconnected); - EasyLoading.showToast('该锁已被重置'.tr, duration: 2000.milliseconds); + if (!F.isSKY) { + EasyLoading.showToast('该锁已被重置'.tr, duration: 2000.milliseconds); + } scanDevices.clear(); BuglyTool.uploadException( @@ -789,34 +795,39 @@ class BlueManage { int retryCount = 0; const int maxRetries = 3; const int retryDelayMs = 500; - + final List valueList = value; final List subData = splitList(valueList, _mtuSize!); - + for (int i = 0; i < subData.length; i++) { // 对每个数据包都应用重试逻辑 bool packetSent = false; retryCount = 0; - + while (!packetSent && retryCount < maxRetries) { try { if (characteristic.properties.writeWithoutResponse) { - await characteristic.write(subData[i], withoutResponse: true); + await characteristic.write(subData[i], + withoutResponse: true); } else if (characteristic.properties.write) { await characteristic.write(subData[i]); } else { // 特性不支持写入 - throw Exception('This characteristic does not support writing.'); + throw Exception( + 'This characteristic does not support writing.'); } - + // 如果到这里没有异常,则包发送成功 packetSent = true; } catch (e) { - if (e.toString().contains('UNKNOWN_GATT_ERROR (133)') && retryCount < maxRetries - 1) { + if (e.toString().contains('UNKNOWN_GATT_ERROR (133)') && + retryCount < maxRetries - 1) { // GATT错误133,尝试重试 retryCount++; - AppLog.log('蓝牙写入失败(GATT 133),数据包 ${i+1}/${subData.length} 正在重试 $retryCount/$maxRetries...'); - await Future.delayed(Duration(milliseconds: retryDelayMs)); + AppLog.log( + '蓝牙写入失败(GATT 133),数据包 ${i + 1}/${subData.length} 正在重试 $retryCount/$maxRetries...'); + await Future.delayed( + Duration(milliseconds: retryDelayMs)); continue; } else { // 其他错误或已达到最大重试次数,抛出异常 @@ -825,12 +836,13 @@ class BlueManage { } } } - + if (!packetSent) { - throw Exception('蓝牙写入失败,数据包 ${i+1}/${subData.length} 已达到最大重试次数'); + throw Exception( + '蓝牙写入失败,数据包 ${i + 1}/${subData.length} 已达到最大重试次数'); } } - + return; // 所有数据包都发送成功 } on Exception catch (e, s) { AppLog.log('APP写入失败: $e $s'); @@ -840,7 +852,7 @@ class BlueManage { } } } - + // 如果找不到合适的特性用于写入 throw Exception('未找到适合写入的蓝牙特性'); } From 77c09923f23ccdc0c0c0fbc59e7f2c115573302c Mon Sep 17 00:00:00 2001 From: liyi Date: Mon, 28 Apr 2025 09:20:25 +0800 Subject: [PATCH 013/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E9=85=8D?= =?UTF-8?q?=E7=BD=91=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuringWifi_logic.dart | 164 +++++++----------- .../configuringWifi/configuringWifi_page.dart | 2 +- .../wifiList/wifiList_logic.dart | 25 +-- 3 files changed, 81 insertions(+), 110 deletions(-) diff --git a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart index 3b23c309..76cc2bf4 100755 --- a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart +++ b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart @@ -181,17 +181,28 @@ class ConfiguringWifiLogic extends BaseGetXController { // 保存到缓存 await Storage.saveLockNetWorkInfo(jsonMap); - await _getUploadLockSet(); + showToast('配网成功'.tr, something: () { + state.sureBtnState.value = 0; // 确保重置状态 + if (state.pageName.value == 'lockSet') { + Get.close(2); + } else { + Get.offAllNamed(Routers.starLockMain); + } + eventBus.fire(SuccessfulDistributionNetwork()); + }); + + // 获取锁设置 + _getUploadLockSet(); } else { dismissEasyLoading(); - showToast('网络配置失败,请重试'.tr); + // showToast('网络配置失败,请重试'.tr); state.sureBtnState.value = 0; } } catch (e) { if (EasyLoading.isShow) { dismissEasyLoading(); } - showToast('解析配网信息失败,请重试'.tr); + // showToast('解析配网信息失败,请重试'.tr); state.sureBtnState.value = 0; // 确保重置状态 AppLog.log('解析配网信息失败: $e'); return; // 添加return阻止后续流程 @@ -203,7 +214,7 @@ class ConfiguringWifiLogic extends BaseGetXController { if (EasyLoading.isShow) { dismissEasyLoading(); } - showToast('WiFi密码错误,请重新输入'.tr); + // showToast('WiFi密码错误,请重新输入'.tr); state.sureBtnState.value = 0; // 确保重置状态 break; @@ -212,7 +223,7 @@ class ConfiguringWifiLogic extends BaseGetXController { if (EasyLoading.isShow) { dismissEasyLoading(); } - showToast('找不到该WiFi网络,请确认WiFi名称正确'.tr); + // showToast('找不到该WiFi网络,请确认WiFi名称正确'.tr); state.sureBtnState.value = 0; // 确保重置状态 break; @@ -221,7 +232,7 @@ class ConfiguringWifiLogic extends BaseGetXController { if (EasyLoading.isShow) { dismissEasyLoading(); } - showToast('连接WiFi超时,请确保网络信号良好'.tr); + // showToast('连接WiFi超时,请确保网络信号良好'.tr); state.sureBtnState.value = 0; // 确保重置状态 break; @@ -230,7 +241,7 @@ class ConfiguringWifiLogic extends BaseGetXController { if (EasyLoading.isShow) { dismissEasyLoading(); } - showToast('配网失败 (错误码: $status),请重试'.tr); + // showToast('配网失败 (错误码: $status),请重试'.tr); state.sureBtnState.value = 0; // 确保重置状态 break; } @@ -315,12 +326,15 @@ class ConfiguringWifiLogic extends BaseGetXController { } // 设置蓝牙操作超时处理 - showBlueConnetctToastTimer(action: () { - if (EasyLoading.isShow) { - dismissEasyLoading(); - } - state.sureBtnState.value = 0; // 连接超时时重置状态 - }); + showBlueConnetctToastTimer( + action: () { + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + state.sureBtnState.value = 0; // 连接超时时重置状态 + }, + outTimer: 30, + ); // 发送配网指令 BlueManage().blueSendData( @@ -340,7 +354,7 @@ class ConfiguringWifiLogic extends BaseGetXController { } cancelBlueConnetctToastTimer(); state.sureBtnState.value = 0; // 发送命令失败时重置状态 - showToast('发送配网指令失败:${e.toString()}'.tr); + // showToast('发送配网指令失败:${e.toString()}'.tr); } } else if (connectionState == BluetoothConnectionState.disconnected) { if (EasyLoading.isShow) { @@ -474,70 +488,53 @@ class ConfiguringWifiLogic extends BaseGetXController { default: //失败 dismissEasyLoading(); - showToast('获取设备状态失败'.tr); + // showToast('获取设备状态失败'.tr); break; } } // 上传数据获取设置 Future _getUploadLockSet() async { - // 保持已有的loading状态,不再重新显示loading - showBlueConnetctToastTimer(action: () { - if (EasyLoading.isShow) { - dismissEasyLoading(); - } - state.sureBtnState.value = 0; - }); - - try { - final List? token = await Storage.getStringList(saveBlueToken); - if (token == null || token.isEmpty) { - throw Exception('Token is empty'); - } - final List getTokenList = changeStringListToIntList(token); - // 蓝牙获取锁设置 - await _uploadLockSet(getTokenList); - } catch (e) { - if (EasyLoading.isShow) { - dismissEasyLoading(); - } - cancelBlueConnetctToastTimer(); - state.sureBtnState.value = 0; - } + final List? token = await Storage.getStringList(saveBlueToken); + final List getTokenList = changeStringListToIntList(token!); + // 蓝牙获取锁设置 + await _uploadLockSet(getTokenList); } // 公共的上传锁设置 Future _uploadLockSet(List token) async { - try { - final List? privateKey = - await Storage.getStringList(saveBluePrivateKey); - if (privateKey == null || privateKey.isEmpty) { - throw Exception('Private key is empty'); - } - final List getPrivateKeyList = changeStringListToIntList(privateKey); + final List? privateKey = + await Storage.getStringList(saveBluePrivateKey); + if (privateKey == null || privateKey.isEmpty) { + throw Exception('Private key is empty'); + } + final List getPrivateKeyList = changeStringListToIntList(privateKey); - final List? signKey = - await Storage.getStringList(saveBlueSignKey); - if (signKey == null || signKey.isEmpty) { - throw Exception('Sign key is empty'); - } - final List signKeyDataList = changeStringListToIntList(signKey); + final List? signKey = await Storage.getStringList(saveBlueSignKey); + if (signKey == null || signKey.isEmpty) { + throw Exception('Sign key is empty'); + } + final List signKeyDataList = changeStringListToIntList(signKey); - IoSenderManage.updataLockSetCommand( + BlueManage().blueSendData(BlueManage().connectDeviceName, + (BluetoothConnectionState connectionState) async { + if (connectionState == BluetoothConnectionState.connected) { + IoSenderManage.updataLockSetCommand( lockID: BlueManage().connectDeviceName, userID: await Storage.getUid(), token: token, needAuthor: 1, signKey: signKeyDataList, - privateKey: getPrivateKeyList); - } catch (e) { - if (EasyLoading.isShow) { + privateKey: getPrivateKeyList, + ); + } else if (connectionState == BluetoothConnectionState.disconnected) { dismissEasyLoading(); + cancelBlueConnetctToastTimer(); + if (state.ifCurrentScreen.value == true) { + showBlueConnetctToast(); + } } - cancelBlueConnetctToastTimer(); - showToast('上传设置失败:${e.toString()}'.tr); - state.sureBtnState.value = 0; - } + }, isAddEquipment: true); } // 上传数据获取锁设置解析 @@ -565,7 +562,7 @@ class ConfiguringWifiLogic extends BaseGetXController { if (EasyLoading.isShow) { dismissEasyLoading(); // 错误时关闭loading } - showToast('获取设置权限失败:${e.toString()}'.tr); + // showToast('获取设置权限失败:${e.toString()}'.tr); state.sureBtnState.value = 0; // 确保重置状态 } break; @@ -574,7 +571,7 @@ class ConfiguringWifiLogic extends BaseGetXController { if (EasyLoading.isShow) { dismissEasyLoading(); // 错误时关闭loading } - showToast('获取锁设置失败 (错误码: $status)'.tr); + // showToast('获取锁设置失败 (错误码: $status)'.tr); state.sureBtnState.value = 0; // 确保重置状态 break; } @@ -585,43 +582,16 @@ class ConfiguringWifiLogic extends BaseGetXController { {required int uploadType, required int recordType, required List records}) async { - try { - final LoginEntity entity = await ApiRepository.to.lockDataUpload( - lockId: state.lockBasicInfo.value.lockId ?? -1, - uploadType: uploadType, - recordType: recordType, - records: records, - isUnShowLoading: true); + final LoginEntity entity = await ApiRepository.to.lockDataUpload( + lockId: state.lockBasicInfo.value.lockId ?? -1, + uploadType: uploadType, + recordType: recordType, + records: records, + isUnShowLoading: true); - if (entity.errorCode!.codeIsSuccessful) { - if (EasyLoading.isShow) { - dismissEasyLoading(); - } - showToast('配网成功'.tr, something: () { - state.sureBtnState.value = 0; // 确保重置状态 - if (state.pageName.value == 'lockSet') { - Get.close(2); - } else { - Get.offAllNamed(Routers.starLockMain); - } - - eventBus.fire( - PassCurrentLockInformationEvent(state.lockSetInfoData.value)); - eventBus.fire(SuccessfulDistributionNetwork()); - }); - } else { - if (EasyLoading.isShow) { - dismissEasyLoading(); - } - // showToast('数据上传失败:${entity.errorCode}'.tr); - state.sureBtnState.value = 0; // 确保重置状态 - } - } catch (e) { - if (EasyLoading.isShow) { - dismissEasyLoading(); - } - // showToast('数据上传异常:${e.toString()}'.tr); - state.sureBtnState.value = 0; // 确保重置状态 + if (entity.errorCode!.codeIsSuccessful) { + eventBus + .fire(PassCurrentLockInformationEvent(state.lockSetInfoData.value)); } } } diff --git a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_page.dart b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_page.dart index 51c45a9d..33a38066 100755 --- a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_page.dart +++ b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_page.dart @@ -51,7 +51,7 @@ class _ConfiguringWifiPageState extends State () => SubmitBtn( btnName: state.sureBtnState.value == 1 ? '配置中...'.tr : '确定'.tr, // 当sureBtnState为1时按钮不可用 - isDisabled: state.sureBtnState.value == 1, + isDisabled: state.sureBtnState.value == 0, onClick: state.sureBtnState.value == 1 ? null : () { diff --git a/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_logic.dart b/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_logic.dart index 6ee5bea6..93532323 100755 --- a/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_logic.dart +++ b/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_logic.dart @@ -37,14 +37,14 @@ class WifiListLogic extends BaseGetXController { }, onError: (error) { // 处理CRC校验失败等错误 AppLog.log('WiFi列表获取过程中发生错误: $error'); - + // 取消loading状态,显示错误提示 dismissEasyLoading(); cancelBlueConnetctToastTimer(); - + // 重置按钮状态,允许重新扫描 state.sureBtnState.value = 0; - + // 如果是CRC校验失败,显示特定提示 if (error.toString().contains('CRC')) { showToast('数据校验失败,请重新扫描'.tr); @@ -91,29 +91,29 @@ class WifiListLogic extends BaseGetXController { // 把得到的数据按33位分割成数组然后处理 final List> getList = splitList(reply.data, 33); final List> uploadList = >[]; - + for (int i = 0; i < getList.length; i++) { final List indexList = getList[i]; final Map indexMap = {}; final List wifiName = indexList.sublist(0, 32); final String wifiNameStr = utf8String(wifiName).trim(); - + // 过滤掉空的WiFi名称 if (wifiNameStr.isEmpty) { continue; } - + indexMap['wifiName'] = wifiNameStr; indexMap['rssi'] = (indexList.last - 255).toString(); uploadList.add(indexMap); } - + // 按信号强度排序WiFi列表 (从强到弱) - uploadList.sort((a, b) => - int.parse(b['rssi']!).compareTo(int.parse(a['rssi']!))); - + uploadList.sort( + (a, b) => int.parse(b['rssi']!).compareTo(int.parse(a['rssi']!))); + state.wifiNameDataList.value = uploadList; - + if (uploadList.isEmpty) { showToast('未检测到可用的WiFi网络'.tr); } @@ -144,7 +144,7 @@ class WifiListLogic extends BaseGetXController { showBlueConnetctToastTimer(action: () { state.sureBtnState.value = 0; }); - + BlueManage().blueSendData(BlueManage().connectDeviceName, (BluetoothConnectionState connectionState) async { if (connectionState == BluetoothConnectionState.connected) { @@ -172,6 +172,7 @@ class WifiListLogic extends BaseGetXController { super.onReady(); _initReplySubscription(); await senderGetWifiListWifiAction(); + dismissEasyLoading(); } @override From 7e813133726d086533b495a72170aa238b537e61 Mon Sep 17 00:00:00 2001 From: liyi Date: Mon, 28 Apr 2025 09:20:52 +0800 Subject: [PATCH 014/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E5=9B=9E?= =?UTF-8?q?=E5=88=B0=E5=88=97=E8=A1=A8=E5=81=B6=E5=B0=94=E5=87=BA=E7=8E=B0?= =?UTF-8?q?=E7=9B=B8=E5=90=8C=E9=94=81=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/main/lockMian/lockList/lockList_logic.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/main/lockMian/lockList/lockList_logic.dart b/lib/main/lockMian/lockList/lockList_logic.dart index dab00451..4ecd4e62 100755 --- a/lib/main/lockMian/lockList/lockList_logic.dart +++ b/lib/main/lockMian/lockList/lockList_logic.dart @@ -60,9 +60,9 @@ class LockListLogic extends BaseGetXController { //设置数据 void setLockListInfoGroupEntity(LockListInfoGroupEntity entity) { this.entity = entity; - if (entity.pageNo == 1) { + // if (entity.pageNo == 1) { _groupDataList = []; - } + // } _groupDataList.addAll(entity.groupList!); update(); } From 51bc375d3188220c5e6f7a916900a2de8f51111c Mon Sep 17 00:00:00 2001 From: liyi Date: Mon, 28 Apr 2025 09:21:11 +0800 Subject: [PATCH 015/151] =?UTF-8?q?fix:=E7=A7=BB=E9=99=A4h264=E3=80=81mjpe?= =?UTF-8?q?g=E5=88=87=E6=8D=A2debug=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lockDetail/lockDetail_logic.dart | 8 --- .../lockDetail/lockDetail_page.dart | 54 ------------------- .../lockDetail/lockDetail_state.dart | 3 -- 3 files changed, 65 deletions(-) diff --git a/lib/main/lockDetail/lockDetail/lockDetail_logic.dart b/lib/main/lockDetail/lockDetail/lockDetail_logic.dart index 72683016..9c11e66b 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_logic.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_logic.dart @@ -798,14 +798,6 @@ class LockDetailLogic extends BaseGetXController { void onInit() { super.onInit(); - // 初始化开关状态为当前对讲视频模式 - final currentTalkExpect = StartChartManage().getDefaultTalkExpect(); - if (currentTalkExpect.videoType.contains(VideoTypeE.H264)) { - state.useH264Mode.value = true; - } else if (currentTalkExpect.videoType.contains(VideoTypeE.IMAGE)) { - state.useH264Mode.value = false; - } - state.LockSetChangeSetRefreshLockDetailWithTypeSubscription = eventBus .on() .listen((LockSetChangeSetRefreshLockDetailWithType event) { diff --git a/lib/main/lockDetail/lockDetail/lockDetail_page.dart b/lib/main/lockDetail/lockDetail/lockDetail_page.dart index 93ea8f2f..ec09e9a3 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_page.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_page.dart @@ -507,60 +507,6 @@ class _LockDetailPageState extends State Widget skWidget() { return ListView( children: [ - // Container( - // padding: EdgeInsets.symmetric(vertical: 15, horizontal: 20), - // margin: EdgeInsets.only(top: 10, bottom: 10), - // decoration: BoxDecoration( - // color: Colors.white, - // borderRadius: BorderRadius.circular(10), - // boxShadow: [ - // BoxShadow( - // color: Colors.black.withOpacity(0.05), - // blurRadius: 5, - // offset: Offset(0, 2), - // ), - // ], - // ), - // child: Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // Text('对讲视频模式'.tr, - // style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), - // Row( - // children: [ - // Text('mjpeg', - // style: TextStyle( - // fontSize: 14, - // color: !state.useH264Mode.value - // ? AppColors.mainColor - // : Colors.grey)), - // Obx(() => Switch( - // value: state.useH264Mode.value, - // activeColor: AppColors.mainColor, - // onChanged: (value) { - // state.useH264Mode.value = value; - // if (value) { - // // 使用H264模式 - // StartChartManage() - // .sendH264VideoAndG711AudioTalkExpectData(); - // } else { - // // 使用Image模式 - // StartChartManage() - // .sendImageVideoAndG711AudioTalkExpectData(); - // } - // }, - // )), - // Text('H264'.tr, - // style: TextStyle( - // fontSize: 14, - // color: state.useH264Mode.value - // ? AppColors.mainColor - // : Colors.grey)), - // ], - // ), - // ], - // ), - // ), Visibility( visible: (state.keyInfos.value.keyType == XSConstantMacro.keyTypeTime || diff --git a/lib/main/lockDetail/lockDetail/lockDetail_state.dart b/lib/main/lockDetail/lockDetail/lockDetail_state.dart index a949b693..44efb77d 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_state.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_state.dart @@ -58,9 +58,6 @@ class LockDetailState { int logCountPage = 10; // 蓝牙记录一页多少个 RxInt nextAuthTime = 0.obs; // 下次认证时间 - // 视频编码模式选择开关状态 - RxBool useH264Mode = true.obs; // true表示使用H264模式,false表示使用Image模式 - // LockDetailState() { // Map map = Get.arguments; // lockCount = map["lockCount"]; From 00b2d036cf48a96587d87cb331d1fc6fd3be5e2e Mon Sep 17 00:00:00 2001 From: liyi Date: Mon, 28 Apr 2025 09:21:50 +0800 Subject: [PATCH 016/151] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E6=92=AD=E6=94=BE=E9=A1=B5=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/appRouters.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/appRouters.dart b/lib/appRouters.dart index 4de686eb..0e989312 100755 --- a/lib/appRouters.dart +++ b/lib/appRouters.dart @@ -1198,7 +1198,7 @@ abstract class AppRouters { page: () => const DoubleLockLinkPage()), GetPage( name: Routers.starChartTalkView, page: () => const TalkViewPage()), - // GetPage(name: Routers.h264WebView, page: () => TalkViewNativeDecodePage()), - GetPage(name: Routers.h264WebView, page: () => H264WebView()), + // GetPage(name: Routers.h264WebView, page: () => TalkViewNativeDecodePage()), // 插件播放页面 + GetPage(name: Routers.h264WebView, page: () => H264WebView()), // webview播放页面 ]; } From 0af232e42c556d7ff1b85a5a7cf284e756838a9f Mon Sep 17 00:00:00 2001 From: liyi Date: Mon, 28 Apr 2025 10:59:48 +0800 Subject: [PATCH 017/151] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E6=97=8B=E8=BD=AC=E8=A7=92=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/html/h264.html | 4 ++-- .../handle/impl/udp_talk_expect_handler.dart | 1 + lib/talk/starChart/star_chart_manage.dart | 2 ++ lib/talk/starChart/views/talkView/talk_view_page.dart | 4 +++- lib/talk/starChart/webView/h264_web_view.dart | 11 +++++++++-- 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/assets/html/h264.html b/assets/html/h264.html index 0995b612..60ec7039 100644 --- a/assets/html/h264.html +++ b/assets/html/h264.html @@ -29,8 +29,8 @@ #player { object-fit: cover; - height: 56vh; - transform: rotate(-90deg); + width: 100vw; + height: 100vh; } diff --git a/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart index d1c83e77..3d8e078d 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart @@ -41,6 +41,7 @@ class UdpTalkExpectHandler extends ScpMessageBaseHandle // 停止发送对讲请求 startChartManage.stopCallRequestMessageTimer(); // talkViewState.rotateAngle.value = talkExpectResp.rotate ?? 0; + startChartManage.rotateAngle = talkExpectResp.rotate; // 收到预期数据的应答后,代表建立了连接,启动通话保持的监听 // 启动通话保持监听定时器(用来判断如果x秒内没有收到通话保持则执行的操作); talkePingOverTimeTimerManager.start(); diff --git a/lib/talk/starChart/star_chart_manage.dart b/lib/talk/starChart/star_chart_manage.dart index 1fb79272..5018bac5 100644 --- a/lib/talk/starChart/star_chart_manage.dart +++ b/lib/talk/starChart/star_chart_manage.dart @@ -112,6 +112,8 @@ class StartChartManage { RbcuConfirm? rbcuConfirm; final int _maxPayloadSize = 8 * 1024; // 分包大小 + int rotateAngle = 0; // 视频旋转角度 + // 默认通话的期望数据格式 TalkExpectReq _defaultTalkExpect = TalkConstant.H264Expect; diff --git a/lib/talk/starChart/views/talkView/talk_view_page.dart b/lib/talk/starChart/views/talkView/talk_view_page.dart index 38b22343..38dd4896 100644 --- a/lib/talk/starChart/views/talkView/talk_view_page.dart +++ b/lib/talk/starChart/views/talkView/talk_view_page.dart @@ -12,6 +12,7 @@ import 'package:star_lock/talk/call/callTalk.dart'; import 'package:star_lock/talk/starChart/constant/talk_status.dart'; import 'package:star_lock/talk/starChart/handle/impl/debug_Info_model.dart'; import 'package:star_lock/talk/starChart/handle/impl/udp_talk_data_handler.dart'; +import 'package:star_lock/talk/starChart/star_chart_manage.dart'; import 'package:star_lock/talk/starChart/views/talkView/talk_view_logic.dart'; import 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart'; @@ -30,6 +31,7 @@ class _TalkViewPageState extends State final TalkViewLogic logic = Get.put(TalkViewLogic()); final TalkViewState state = Get.find().state; late Stream _latencyStream; + final startChartManage = StartChartManage(); @override void initState() { @@ -132,7 +134,7 @@ class _TalkViewPageState extends State key: state.globalKey, child: SizedBox.expand( child: RotatedBox( - quarterTurns: -1, + quarterTurns: startChartManage.remotePort ~/ 90, child: Obx( () => state.currentImage.value != null ? RawImage( diff --git a/lib/talk/starChart/webView/h264_web_view.dart b/lib/talk/starChart/webView/h264_web_view.dart index aa18aa3c..03f97609 100644 --- a/lib/talk/starChart/webView/h264_web_view.dart +++ b/lib/talk/starChart/webView/h264_web_view.dart @@ -26,6 +26,7 @@ class _H264WebViewState extends State with TickerProviderStateMixin { final H264WebViewLogic logic = Get.put(H264WebViewLogic()); final H264WebViewState state = Get.find().state; + final startChartManage = StartChartManage(); @override void initState() { @@ -72,8 +73,13 @@ class _H264WebViewState extends State height: screenHeight, fit: BoxFit.cover, ) - : WebViewWidget( - controller: state.webViewController, + : SizedBox.expand( + child: RotatedBox( + quarterTurns: startChartManage.remotePort ~/ 90, + child: WebViewWidget( + controller: state.webViewController, + ), + ), ); }), Obx( @@ -412,6 +418,7 @@ class _H264WebViewState extends State ), ); } + @override void dispose() { state.animationController.dispose(); From a546989a253350e00b6e73e63da787670f88f871 Mon Sep 17 00:00:00 2001 From: liyi Date: Mon, 28 Apr 2025 11:47:26 +0800 Subject: [PATCH 018/151] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E6=97=8B=E8=BD=AC=E8=A7=92=E5=BA=A6=EF=BC=8C=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E7=9B=91=E6=8E=A7=E6=97=B6=E8=8E=B7=E5=8F=96=E9=94=81?= =?UTF-8?q?peerId=E7=9A=84=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lockDetail/lockDetail_logic.dart | 61 +++++++++---------- .../handle/impl/udp_talk_expect_handler.dart | 2 + .../handle/impl/udp_talk_request_handler.dart | 12 ++-- lib/talk/starChart/star_chart_manage.dart | 42 +++++++++---- .../native/talk_view_native_decode_logic.dart | 28 ++++++--- .../views/talkView/talk_view_page.dart | 2 +- lib/talk/starChart/webView/h264_web_view.dart | 2 +- 7 files changed, 91 insertions(+), 58 deletions(-) diff --git a/lib/main/lockDetail/lockDetail/lockDetail_logic.dart b/lib/main/lockDetail/lockDetail/lockDetail_logic.dart index 9c11e66b..7c086ba5 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_logic.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_logic.dart @@ -743,43 +743,40 @@ class LockDetailLogic extends BaseGetXController { eventBus.fire(RefreshLockDetailInfoDataEvent()); } - /// 请求设备网络信息并设置 - void requestDeviceNetworkInfo() async { - final DeviceNetwork deviceNetworkInfo = - await ApiRepository.to.getDeviceNetwork( - deviceType: 2, - deviceMac: state.keyInfos.value.mac!, - ); - if (deviceNetworkInfo.data?.wifiName == null || - deviceNetworkInfo.data?.wifiName == '') { - return; - } else { - final peerId = deviceNetworkInfo?.data?.peerId; - if (peerId == null || peerId.isEmpty || peerId == '') { - throw Exception('设备peerId为空'); - } - // 设置锁的peerID - StartChartManage().lockNetworkInfo = - deviceNetworkInfo.data ?? DeviceNetworkInfo(); - StartChartManage().lockPeerId = peerId; - } - } + // /// 请求设备网络信息并设置 + // void requestDeviceNetworkInfo() async { + // final DeviceNetwork deviceNetworkInfo = + // await ApiRepository.to.getDeviceNetwork( + // deviceType: 2, + // deviceMac: state.keyInfos.value.mac!, + // ); + // if (deviceNetworkInfo.data?.peerId == null || + // deviceNetworkInfo.data?.peerId == '') { + // return; + // } + // final peerId = deviceNetworkInfo!.data!.peerId; + // // 设置锁的peerID + // StartChartManage().lockNetworkInfo = + // deviceNetworkInfo.data ?? DeviceNetworkInfo(); + // StartChartManage().lockPeerId = peerId!; + // } /// 发送监控消息 void sendMonitorMessage() { final catEyeConfig = state.keyInfos.value.lockSetting?.catEyeConfig ?? []; + final network = state.keyInfos.value.network; if (catEyeConfig.isNotEmpty && catEyeConfig.length > 0 && catEyeConfig[0].catEyeMode != 0) { - if ((StartChartManage().lockNetworkInfo.wifiName == null || - StartChartManage().lockNetworkInfo.wifiName == '') ) { + if (network == null || network?.peerId == null || network?.peerId == '') { showToast('设备未配网'.tr); return; } + // 重置丢包率监控 PacketLossStatistics().reset(); // 发送监控id - StartChartManage().startCallRequestMessageTimer( - ToPeerId: StartChartManage().lockNetworkInfo.peerId ?? ''); + StartChartManage() + .startCallRequestMessageTimer(ToPeerId: network!.peerId ?? ''); } else { showToast('猫眼设置为省电模式时无法进行监控,请在猫眼设置中切换为其他模式'.tr); } @@ -791,7 +788,7 @@ class LockDetailLogic extends BaseGetXController { getServerDatetime(); await PermissionDialog.request(Permission.location); await PermissionDialog.requestBluetooth(); - requestDeviceNetworkInfo(); + // requestDeviceNetworkInfo(); } @override @@ -844,11 +841,11 @@ class LockDetailLogic extends BaseGetXController { } }); - state.SuccessfulDistributionNetworkEvent = eventBus - .on() - .listen((SuccessfulDistributionNetwork event) { - // 配网成功获取一下配网信息 - requestDeviceNetworkInfo(); - }); + // state.SuccessfulDistributionNetworkEvent = eventBus + // .on() + // .listen((SuccessfulDistributionNetwork event) { + // // 配网成功获取一下配网信息 + // requestDeviceNetworkInfo(); + // }); } } diff --git a/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart index 3d8e078d..e9f3e598 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:flutter/services.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:get/get.dart'; +import 'package:star_lock/app_settings/app_settings.dart'; import 'package:star_lock/talk/starChart/constant/message_type_constant.dart'; import 'package:star_lock/talk/starChart/constant/talk_status.dart'; import 'package:star_lock/talk/starChart/entity/scp_message.dart'; @@ -42,6 +43,7 @@ class UdpTalkExpectHandler extends ScpMessageBaseHandle startChartManage.stopCallRequestMessageTimer(); // talkViewState.rotateAngle.value = talkExpectResp.rotate ?? 0; startChartManage.rotateAngle = talkExpectResp.rotate; + AppLog.log('视频画面需要旋转:${talkExpectResp.rotate}'); // 收到预期数据的应答后,代表建立了连接,启动通话保持的监听 // 启动通话保持监听定时器(用来判断如果x秒内没有收到通话保持则执行的操作); talkePingOverTimeTimerManager.start(); diff --git a/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart index 91fdcc83..3a26c1fa 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart @@ -180,15 +180,15 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle if (isH264) { // 锁支持H264,发送H264视频和G711音频期望 startChartManage.sendOnlyH264VideoTalkExpectData(); - print('锁支持H264,发送H264视频格式期望数据'); + print('app收到的对讲请求后,发送的预期数据=========锁支持H264,发送H264视频格式期望数据'); } else if (isMJpeg) { // 锁只支持MJPEG,发送图像视频和G711音频期望 startChartManage.sendOnlyImageVideoTalkExpectData(); - print('锁不支持H264,支持MJPEG,发送MJPEG视频格式期望数据'); + print('app收到的对讲请求后,发送的预期数据=========锁不支持H264,支持MJPEG,发送MJPEG视频格式期望数据'); } else { // 默认使用图像视频 startChartManage.sendOnlyImageVideoTalkExpectData(); - print('锁不支持H264和MJPEG,默认发送图像视频格式期望数据'); + print('app收到的对讲请求后,发送的预期数据=========锁不支持H264和MJPEG,默认发送图像视频格式期望数据'); } } @@ -203,15 +203,15 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle if (isH264) { // 锁支持H264,发送H264视频和G711音频期望 startChartManage.sendH264VideoAndG711AudioTalkExpectData(); - print('锁支持H264,发送H264视频格式期望数据'); + print('app主动发请求,收到回复后发送的预期数据=======锁支持H264,发送H264视频格式期望数据'); } else if (isMJpeg) { // 锁只支持MJPEG,发送图像视频和G711音频期望 startChartManage.sendImageVideoAndG711AudioTalkExpectData(); - print('锁不支持H264,支持MJPEG,发送MJPEG视频格式期望数据'); + print('app主动发请求,收到回复后发送的预期数据=======锁不支持H264,支持MJPEG,发送MJPEG视频格式期望数据'); } else { // 默认使用图像视频 startChartManage.sendImageVideoAndG711AudioTalkExpectData(); - print('锁不支持H264和MJPEG,默认发送图像视频格式期望数据'); + print('app主动发请求,收到回复后发送的预期数据=======锁不支持H264和MJPEG,默认发送图像视频格式期望数据'); } } } diff --git a/lib/talk/starChart/star_chart_manage.dart b/lib/talk/starChart/star_chart_manage.dart index 5018bac5..e5eddb59 100644 --- a/lib/talk/starChart/star_chart_manage.dart +++ b/lib/talk/starChart/star_chart_manage.dart @@ -45,6 +45,7 @@ import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart'; import 'package:star_lock/talk/starChart/proto/talk_expect.pbserver.dart'; import 'package:star_lock/talk/starChart/status/star_chart_talk_status.dart'; import 'package:star_lock/tools/baseGetXController.dart'; +import 'package:star_lock/tools/commonDataManage.dart'; import 'package:star_lock/tools/deviceInfo_utils.dart'; import 'package:star_lock/tools/storage.dart'; import 'package:uuid/uuid.dart'; @@ -407,17 +408,36 @@ class StartChartManage { /// 启动持续发送对讲请求 void startCallRequestMessageTimer({required String ToPeerId}) async { // 如果已经处于等待接听状态就不发送 - if (talkStatus.status != TalkStatus.proactivelyCallWaitingAnswer) { - // 如果是h264则跳转至webview - if (_defaultTalkExpect.videoType.contains(VideoTypeE.H264)) { - Get.toNamed( - Routers.h264WebView, - ); - } else { - Get.toNamed( - Routers.starChartTalkView, - ); - } + // if (talkStatus.status != TalkStatus.proactivelyCallWaitingAnswer) { + // // 如果是h264则跳转至webview + // if (_defaultTalkExpect.videoType.contains(VideoTypeE.H264)) { + // Get.toNamed( + // Routers.h264WebView, + // ); + // } else { + // Get.toNamed( + // Routers.starChartTalkView, + // ); + // } + // } + final LockListInfoItemEntity currentKeyInfo = + CommonDataManage().currentKeyInfo; + final isH264 = currentKeyInfo.lockFeature?.isH264 == 1; + final isMJpeg = currentKeyInfo.lockFeature?.isMJpeg == 1; + + // 优先使用H264,其次是MJPEG + if (isH264) { + Get.toNamed( + Routers.h264WebView, + ); + } else if (isMJpeg) { + Get.toNamed( + Routers.starChartTalkView, + ); + } else { + Get.toNamed( + Routers.starChartTalkView, + ); } // 启动定时器持续发送对讲请求 talkRequestTimer ??= Timer.periodic( diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart index cf76414e..2fad25a7 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart @@ -59,6 +59,9 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { // 添加一个集合来跟踪已成功解码的I帧序号 final Set _decodedIFrames = {}; + // 定义一个变量来保存上一帧的时间戳 + int? _previousFrameTimestamp; + // 初始化视频解码器 Future _initVideoDecoder() async { try { @@ -227,16 +230,27 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { _playAudioFrames(); break; case TalkData_ContentTypeE.H264: + // 获取当前时间戳 + final currentTimestamp = DateTime.now().millisecondsSinceEpoch; + + // 如果存在上一帧时间戳,则计算时间间隔 + if (_previousFrameTimestamp != null) { + final timeDifference = currentTimestamp - _previousFrameTimestamp!; + AppLog.log('当前帧与上一帧的时间间隔: $timeDifference 毫秒'); + } + // 更新上一帧时间戳为当前帧的时间戳 + _previousFrameTimestamp = currentTimestamp; + // 添加到视频帧缓冲区,而不是直接处理 // _processH264Frame(talkData, talkDataH264Frame!); // 直接处理H264视频帧 - _processH264Frame(talkData, talkDataH264Frame!); - - // 记录关键调试信息 - if (talkDataH264Frame!.frameType == TalkDataH264Frame_FrameTypeE.I) { - AppLog.log( - '帧序号${talkDataH264Frame.frameSeq};帧类型:${talkDataH264Frame.frameType.toString()};时间戳:${DateTime.now().millisecondsSinceEpoch}'); - } + // _processH264Frame(talkData, talkDataH264Frame!); + // + // // 记录关键调试信息 + // if (talkDataH264Frame!.frameType == TalkDataH264Frame_FrameTypeE.I) { + // AppLog.log( + // '帧序号${talkDataH264Frame.frameSeq};帧类型:${talkDataH264Frame.frameType.toString()};时间戳:${DateTime.now().millisecondsSinceEpoch}'); + // } break; } }); diff --git a/lib/talk/starChart/views/talkView/talk_view_page.dart b/lib/talk/starChart/views/talkView/talk_view_page.dart index 38dd4896..f9159cee 100644 --- a/lib/talk/starChart/views/talkView/talk_view_page.dart +++ b/lib/talk/starChart/views/talkView/talk_view_page.dart @@ -134,7 +134,7 @@ class _TalkViewPageState extends State key: state.globalKey, child: SizedBox.expand( child: RotatedBox( - quarterTurns: startChartManage.remotePort ~/ 90, + quarterTurns: startChartManage.rotateAngle ~/ 90, child: Obx( () => state.currentImage.value != null ? RawImage( diff --git a/lib/talk/starChart/webView/h264_web_view.dart b/lib/talk/starChart/webView/h264_web_view.dart index 03f97609..f0f11885 100644 --- a/lib/talk/starChart/webView/h264_web_view.dart +++ b/lib/talk/starChart/webView/h264_web_view.dart @@ -75,7 +75,7 @@ class _H264WebViewState extends State ) : SizedBox.expand( child: RotatedBox( - quarterTurns: startChartManage.remotePort ~/ 90, + quarterTurns: startChartManage.rotateAngle ~/ 90, child: WebViewWidget( controller: state.webViewController, ), From 5968c4a98064c724038e9f2907b5eda7f895407c Mon Sep 17 00:00:00 2001 From: liyi Date: Wed, 30 Apr 2025 10:25:19 +0800 Subject: [PATCH 019/151] =?UTF-8?q?fix:=E5=AE=8C=E6=88=90720P=2020?= =?UTF-8?q?=E5=B8=A7=E6=B8=B2=E6=9F=93=E9=9C=80=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../starChart/proto/talk_expect.pbenum.dart | 2 + .../native/talk_view_native_decode_logic.dart | 921 +++++++++++++----- .../native/talk_view_native_decode_page.dart | 194 ++-- .../native/talk_view_native_decode_state.dart | 15 + 4 files changed, 774 insertions(+), 358 deletions(-) diff --git a/lib/talk/starChart/proto/talk_expect.pbenum.dart b/lib/talk/starChart/proto/talk_expect.pbenum.dart index d6b34250..10306109 100644 --- a/lib/talk/starChart/proto/talk_expect.pbenum.dart +++ b/lib/talk/starChart/proto/talk_expect.pbenum.dart @@ -19,12 +19,14 @@ class VideoTypeE extends $pb.ProtobufEnum { 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 VideoTypeE H264_720P = VideoTypeE._(4, _omitEnumNames ? '' : 'H264_720P'); static const $core.List values = [ NONE_V, H264, IMAGE, VP8, + H264_720P, ]; static final $core.Map<$core.int, VideoTypeE> _byValue = $pb.ProtobufEnum.initByValue(values); diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart index 2fad25a7..8129f204 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'dart:ui' as ui; import 'dart:math'; // Import the math package to use sqrt +import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; @@ -62,41 +63,76 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { // 定义一个变量来保存上一帧的时间戳 int? _previousFrameTimestamp; + int _flutterToNativeFrameCount = 0; + int _lastFlutterToNativePrintTime = 0; + int _networkFrameCount = 0; + int _lastNetworkPrintTime = 0; + Timer? _frameRefreshTimer; + + bool _isFrameAvailable = true; + int _renderedFrameCount = 0; + int _lastRenderedFrameTime = 0; + + // 写入前的缓存队列(I帧前) + final List> _preIFrameCache = []; + bool _hasWrittenFirstIFrame = false; + + bool _isStartNative = false; + + // 新增:SPS/PPS状态追踪变量 + bool hasSps = false; + bool hasPps = false; + + // 新增:SPS/PPS缓存 + List? spsCache; + List? ppsCache; + + void _setupFrameRefresh() { + // 设置帧刷新定时器,16ms对应约60fps + _frameRefreshTimer = + Timer.periodic(const Duration(milliseconds: 16), (timer) { + if (_isFrameAvailable) { + _isFrameAvailable = false; + _renderedFrameCount++; + + // 每秒统计一次帧率 + int now = DateTime.now().millisecondsSinceEpoch; + if (now - _lastRenderedFrameTime > 1000) { + print('[Flutter] 每秒渲染帧数: $_renderedFrameCount'); + _renderedFrameCount = 0; + _lastRenderedFrameTime = now; + } + + // 请求Flutter重建widget + WidgetsBinding.instance.scheduleFrame(); + } + }); + } + + void onFrameAvailable() { + _isFrameAvailable = true; + } + // 初始化视频解码器 Future _initVideoDecoder() async { try { // 创建解码器配置 final config = VideoDecoderConfig( - width: 864, + width: 1280, // 实际视频宽度 - height: 480, - frameRate: 25, - // 明确设置帧率 - // 增大缓冲区大小 - codecType: CodecType.h264, - // 编解码类型 - isDebug: true, + height: 720, + codecType: 'h264', ); - // 初始化解码器并获取textureId final textureId = await VideoDecodePlugin.initDecoder(config); - if (textureId != null) { state.textureId.value = textureId; AppLog.log('视频解码器初始化成功:textureId=$textureId'); - - // 设置帧回调 - VideoDecodePlugin.setFrameCallback(_onFrameAvailable); - - // 设置状态回调 - VideoDecodePlugin.setStateCallbackForTexture( - textureId, _onDecoderStateChanged); - - // 启动FPS监测 - startFpsMonitoring(); + // 启动帧处理定时器 } else { AppLog.log('视频解码器初始化失败'); } + _startFrameProcessTimer(); } catch (e) { AppLog.log('初始化视频解码器错误: $e'); // 如果初始化失败,延迟后重试 @@ -108,66 +144,6 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { } } - // 添加帧可用回调 - void _onFrameAvailable(int textureId) {} - - // 解码器状态变化回调 - void _onDecoderStateChanged( - int textureId, DecoderState decoderState, Map stats) { - String stateText; - switch (decoderState) { - case DecoderState.initializing: - state.isLoading.value = true; - stateText = "初始化中"; - break; - case DecoderState.ready: - stateText = "准备就绪"; - break; - case DecoderState.rendering: - stateText = "渲染中"; - state.isLoading.value = false; - break; - case DecoderState.error: - stateText = "出错"; - // 获取错误信息 - final errorMessage = stats['errorMessage'] as String?; - if (errorMessage != null) { - AppLog.log("解码器错误: $errorMessage"); - } - break; - case DecoderState.released: - stateText = "已释放"; - break; - default: - stateText = "未知状态"; - } - - // 更新统计信息 - if (stats.isNotEmpty) { - // 获取丢包率统计信息 - final PacketLossInfo packetLossInfo = - PacketLossStatistics().getStatistics(); - - // 更新FPS - // state.decoderFps.value = (stats['fps'] as num?)?.toDouble() ?? 0.0; - // 更新解码器详细信息 - state.renderedFrameCount.value = (stats['renderedFrames'] as int?) ?? 0; - state.totalFrames.value = (stats['totalFrames'] as int?) ?? 0; - state.droppedFrames.value = (stats['droppedFrames'] as int?) ?? 0; - state.hasSentIDR.value = (stats['hasSentIDR'] as bool?) ?? false; - state.hasSentSPS.value = (stats['hasSentSPS'] as bool?) ?? false; - state.hasSentPPS.value = (stats['hasSentPPS'] as bool?) ?? false; - state.keyFrameInterval.value = (stats['keyFrameInterval'] as int?) ?? 0; - state.decodingJitterMs.value = (stats['decodingJitterMs'] as int?) ?? 0; - - // 更新状态数据 - state.messageLossRate.value = packetLossInfo.messageLossRate; - state.packetLossRate.value = packetLossInfo.packetLossRate; - state.lastPacketStatsUpdateTime.value = - DateTime.now().millisecondsSinceEpoch; - } - } - /// 初始化音频播放器 void _initFlutterPcmSound() { const int sampleRate = 8000; @@ -190,13 +166,149 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { // 拒绝 StartChartManage().startTalkRejectMessageTimer(); } - if (state.textureId.value != null) { - VideoDecodePlugin.releaseDecoderForTexture(state.textureId.value!); - } - VideoDecodePlugin.releaseAllDecoders(); + VideoDecodePlugin.releaseDecoder(); Get.back(); } + /// 添加H264帧到缓冲区 + void _addFrameToBuffer( + List frameData, + TalkDataH264Frame_FrameTypeE frameType, + int pts, + int frameSeq, + int frameSeqI, + ) { + _networkFrameCount++; + int now = DateTime.now().millisecondsSinceEpoch; + if (now - _lastNetworkPrintTime > 1000) { + AppLog.log('[Flutter] 每秒收到网络H264帧数: ' + _networkFrameCount.toString()); + state.networkH264Fps.value = _networkFrameCount; + _networkFrameCount = 0; + _lastNetworkPrintTime = now; + } + + // 创建包含帧数据和类型的Map + final Map frameMap = { + 'frameData': frameData, + 'frameType': frameType, + 'frameSeq': frameSeq, + 'frameSeqI': frameSeqI, + 'pts': pts, + }; + + // 将帧添加到缓冲区 + state.h264FrameBuffer.add(frameMap); + + // 如果缓冲区超出最大大小,移除最早的帧 + while (state.h264FrameBuffer.length > state.maxFrameBufferSize) { + state.h264FrameBuffer.removeAt(0); + } + + _flutterToNativeFrameCount++; + if (now - _lastFlutterToNativePrintTime > 1000) { + AppLog.log( + '[Flutter] 每秒送入Native帧数: ' + _flutterToNativeFrameCount.toString()); + state.nativeSendFps.value = _flutterToNativeFrameCount; + _flutterToNativeFrameCount = 0; + _lastFlutterToNativePrintTime = now; + } + } + + /// 启动帧处理定时器 + void _startFrameProcessTimer() { + // 取消已有定时器 + state.frameProcessTimer?.cancel(); + + // 计算定时器间隔,确保以目标帧率处理帧 + final int intervalMs = (1000 / state.targetFps).round(); + + // 创建新定时器 + state.frameProcessTimer = + Timer.periodic(Duration(milliseconds: intervalMs), (timer) { + if (state.isLoading.isTrue) { + state.isLoading.value = false; + } + _processNextFrameFromBuffer(); + }); + + AppLog.log('启动帧处理定时器,目标帧率: ${state.targetFps}fps,间隔: ${intervalMs}ms'); + } + + /// 从缓冲区处理下一帧 + void _processNextFrameFromBuffer() async { + // 避免重复处理 + if (state.isProcessingFrame) { + return; + } + + // 如果缓冲区为空,跳过 + if (state.h264FrameBuffer.isEmpty) { + return; + } + + // 设置正在处理标志 + state.isProcessingFrame = true; + + try { + // 取出最早的帧 + final Map frameMap = state.h264FrameBuffer.removeAt(0); + final List frameData = frameMap['frameData']; + final TalkDataH264Frame_FrameTypeE frameType = frameMap['frameType']; + final int frameSeq = frameMap['frameSeq']; + final int frameSeqI = frameMap['frameSeqI']; + int pts = DateTime.now().millisecondsSinceEpoch; + + if (frameType == TalkDataH264Frame_FrameTypeE.P) { + // 以frameSeqI为I帧序号标识 + if (!(_decodedIFrames.contains(frameSeqI))) { + AppLog.log('丢弃P帧:未收到对应I帧,frameSeqI=${frameSeqI}'); + return; + } + } else if (frameType == TalkDataH264Frame_FrameTypeE.I) { + // 记录已解码I帧序号 + _decodedIFrames.add(frameSeq); + } + // 实时写入h264文件 + _appendH264FrameToFile(frameData, frameType); + + final timestamp = DateTime.now().microsecondsSinceEpoch; + VideoDecodePlugin.decodeFrame( + frameData: Uint8List.fromList(frameData), + frameType: frameType == TalkDataH264Frame_FrameTypeE.I ? 1 : 0, + frameSeq: frameSeq, + timestamp: timestamp, + refIFrameSeq: frameSeqI, + ); + + // 判断P帧对应I帧是否已解码,未解码则丢弃P帧 + if (frameType == TalkDataH264Frame_FrameTypeE.P) { + // 以frameSeqI为I帧序号标识 + if (!(_decodedIFrames.contains(frameSeqI))) { + AppLog.log('丢弃P帧:未收到对应I帧,frameSeqI=${frameSeqI}'); + } + } else if (frameType == TalkDataH264Frame_FrameTypeE.I) { + // 记录已解码I帧序号 + _decodedIFrames.add(frameSeq); + } + // 实时写入h264文件 + _appendH264FrameToFile(frameData, frameType); + } catch (e) { + AppLog.log('处理缓冲帧失败: $e'); + } finally { + // 重置处理标志 + state.isProcessingFrame = false; + } + } + + /// 停止帧处理定时器 + void _stopFrameProcessTimer() { + state.frameProcessTimer?.cancel(); + state.frameProcessTimer = null; + state.h264FrameBuffer.clear(); + state.isProcessingFrame = false; + // AppLog.log('停止帧处理定时器'); + } + // 发起接听命令 void initiateAnswerCommand() { StartChartManage().startTalkAcceptTimer(); @@ -230,82 +342,63 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { _playAudioFrames(); break; case TalkData_ContentTypeE.H264: - // 获取当前时间戳 - final currentTimestamp = DateTime.now().millisecondsSinceEpoch; - - // 如果存在上一帧时间戳,则计算时间间隔 - if (_previousFrameTimestamp != null) { - final timeDifference = currentTimestamp - _previousFrameTimestamp!; - AppLog.log('当前帧与上一帧的时间间隔: $timeDifference 毫秒'); - } - // 更新上一帧时间戳为当前帧的时间戳 - _previousFrameTimestamp = currentTimestamp; - - // 添加到视频帧缓冲区,而不是直接处理 - // _processH264Frame(talkData, talkDataH264Frame!); - // 直接处理H264视频帧 - // _processH264Frame(talkData, talkDataH264Frame!); - // - // // 记录关键调试信息 - // if (talkDataH264Frame!.frameType == TalkDataH264Frame_FrameTypeE.I) { - // AppLog.log( - // '帧序号${talkDataH264Frame.frameSeq};帧类型:${talkDataH264Frame.frameType.toString()};时间戳:${DateTime.now().millisecondsSinceEpoch}'); + // if (_isStartNative) { + // if (talkDataH264Frame != null) { + // // 如果是I帧,先分割NALU,找到SPS/PPS并优先放入缓冲区 + // if (talkDataH264Frame.frameType == + // TalkDataH264Frame_FrameTypeE.I) { + // // 清空缓冲区,丢弃I帧前所有未处理帧(只保留SPS/PPS/I帧) + // state.h264FrameBuffer.clear(); + // _extractAndBufferSpsPpsForBuffer( + // talkData.content, + // talkData.durationMs, + // talkDataH264Frame.frameSeq, + // talkDataH264Frame.frameSeqI); + // } + // _addFrameToBuffer( + // talkData.content, + // talkDataH264Frame.frameType, + // talkData.durationMs, + // talkDataH264Frame.frameSeq, + // talkDataH264Frame.frameSeqI); + // } + // } else { + // await VideoDecodePlugin.startNativePlayer( + // VideoDecoderConfig(width: 1280, height: 720, codecType: 'h264'), + // ); + // _isStartNative = true; // } + // 处理H264帧 + if (state.textureId.value != null) { + if (talkDataH264Frame != null) { + if (talkDataH264Frame.frameType == + TalkDataH264Frame_FrameTypeE.I) { + _handleIFrameWithSpsPpsAndIdr( + talkData.content, + talkData.durationMs, + talkDataH264Frame.frameSeq, + talkDataH264Frame.frameSeqI, + ); + return; + } else if (talkDataH264Frame.frameType == + TalkDataH264Frame_FrameTypeE.P) { + _handlePFrame( + talkData.content, + talkData.durationMs, + talkDataH264Frame.frameSeq, + talkDataH264Frame.frameSeqI, + ); + return; + } + } + } else { + AppLog.log('无法处理H264帧:textureId为空'); + } break; } }); } - // 处理H264视频帧 - Future _processH264Frame( - TalkData talkData, TalkDataH264Frame frameInfo) async { - // 检查解码器是否已初始化 - if (state.textureId.value == null) { - // 可以记录日志或尝试初始化解码器 - AppLog.log('解码器尚未初始化,尝试重新初始化...'); - await _initVideoDecoder(); - - // 如果仍未初始化成功,则丢弃此帧 - if (state.textureId.value == null) { - return; - } - } - // 获取P帧对应的I帧序号 - final frameSeqI = frameInfo.frameSeqI; - - // P帧检查:如果依赖的I帧未解码成功,直接丢弃 - if (frameInfo.frameType == TalkDataH264Frame_FrameTypeE.P && - !_decodedIFrames.contains(frameSeqI)) { - AppLog.log('丢弃P帧: 依赖的I帧(${frameSeqI})尚未解码, P帧序号: ${frameInfo.frameSeq}'); - return; - } - - // 从talkData中提取H264帧数据 - final Uint8List frameData = Uint8List.fromList(talkData.content); - - // 确定帧类型 - final FrameType frameType = - frameInfo.frameType == TalkDataH264Frame_FrameTypeE.I - ? FrameType.iFrame - : FrameType.pFrame; - - // 将帧数据交给解码器处理 - try { - final bool result = - await VideoDecodePlugin.decodeFrame(frameData, frameType); - // 如果是I帧且成功解码,将其序号加入已解码I帧集合 - if (frameInfo.frameType == TalkDataH264Frame_FrameTypeE.I && result) { - _decodedIFrames.add(frameInfo.frameSeq); - // 限制集合大小,避免内存泄漏 - if (_decodedIFrames.length > 30) { - _decodedIFrames.remove(_decodedIFrames.first); - } - } - } catch (e) { - AppLog.log('解码帧错误: $e, 帧序号: ${frameInfo.frameSeq}'); - } - } - // 新增:音频帧播放逻辑 void _playAudioFrames() { // 如果缓冲区为空或未达到目标大小,不进行播放 @@ -462,10 +555,20 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { // 初始化视频解码器 _initVideoDecoder(); + + // 初始化H264帧缓冲区 + state.h264FrameBuffer.clear(); + state.isProcessingFrame = false; + + _setupFrameRefresh(); } @override void onClose() { + _closeH264File(); + // 停止帧处理定时器 + _stopFrameProcessTimer(); + _stopPlayG711Data(); // 停止播放音频 state.audioBuffer.clear(); // 清空音频缓冲区 @@ -490,11 +593,9 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { _streamSubscription?.cancel(); _isListening = false; - // 停止FPS监测 - stopFpsMonitoring(); // 重置期望数据 StartChartManage().reSetDefaultTalkExpect(); - VideoDecodePlugin.releaseAllDecoders(); + VideoDecodePlugin.releaseDecoder(); // 取消批处理定时器 _batchProcessTimer?.cancel(); @@ -502,7 +603,8 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { // 清空已解码I帧集合 _decodedIFrames.clear(); - + _frameRefreshTimer?.cancel(); + _frameRefreshTimer = null; super.onClose(); } @@ -722,91 +824,430 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { return result; } - // 启动网络状态监测 - void startFpsMonitoring() { - // 确保只有一个计时器在运行 - stopFpsMonitoring(); - - // 初始化时间记录 - state.lastFpsUpdateTime.value = DateTime.now().millisecondsSinceEpoch; - - // 创建一个计时器,每秒更新一次丢包率和性能数据 - state.fpsTimer = Timer.periodic(const Duration(seconds: 1), (timer) { - // 更新丢包率数据 - updatePacketLossStats(); - - // 分析性能数据 - _analyzePerformance(); - }); - } - - // 停止网络状态监测 - void stopFpsMonitoring() { - state.fpsTimer?.cancel(); - state.fpsTimer = null; - } - - // 日志记录方法 - void logMessage(String message) { - AppLog.log(message); - } - - // 更新丢包率统计数据 - void updatePacketLossStats() async { - try {} catch (e) { - logMessage('获取丢包率数据失败: $e'); - } - } - - // 添加性能分析方法 - void _analyzePerformance() { - final int now = DateTime.now().millisecondsSinceEpoch; - - // 如果是首次调用,初始化数据 - if (state.lastPerformanceCheck == 0) { - state.lastPerformanceCheck = now; - state.lastFrameCount = state.renderedFrameCount.value; - return; - } - - // 每秒分析一次性能 - if (now - state.lastPerformanceCheck >= 1000) { - // 计算过去一秒的实际帧率 - final int frameRendered = - state.renderedFrameCount.value - state.lastFrameCount; - final double actualFPS = - frameRendered * 1000 / (now - state.lastPerformanceCheck); - - // 计算丢帧率 - final double dropRate = state.droppedFrames.value / - (state.totalFrames.value > 0 ? state.totalFrames.value : 1) * - 100; - - // 计算当前解码器积压帧数 - final int pendingFrames = - state.totalFrames.value - state.renderedFrameCount.value; - - // 计算跟踪Map中的帧数(正在处理中的帧) - final int processingFrames = state.frameTracker.length; - - // 分析渲染瓶颈 - String performanceStatus = "正常"; - if (actualFPS < 15 && dropRate > 10) { - performanceStatus = "严重渲染瓶颈"; - } else if (actualFPS < 20 && dropRate > 5) { - performanceStatus = "轻微渲染瓶颈"; + /// 追加写入一帧到h264文件(需传入帧数据和帧类型frameType) + Future _appendH264FrameToFile( + List frameData, TalkDataH264Frame_FrameTypeE frameType) async { + try { + if (state.h264File == null) { + await _initH264File(); + } + // NALU分割函数,返回每个NALU的完整字节数组 + List> splitNalus(List data) { + List> nalus = []; + int i = 0; + while (i < data.length - 3) { + int start = -1; + int next = -1; + if (data[i] == 0x00 && data[i + 1] == 0x00) { + if (data[i + 2] == 0x01) { + start = i; + i += 3; + } else if (i + 3 < data.length && + data[i + 2] == 0x00 && + data[i + 3] == 0x01) { + start = i; + i += 4; + } else { + i++; + continue; + } + next = i; + while (next < data.length - 3) { + if (data[next] == 0x00 && + data[next + 1] == 0x00 && + ((data[next + 2] == 0x01) || + (data[next + 2] == 0x00 && data[next + 3] == 0x01))) { + break; + } + next++; + } + nalus.add(data.sublist(start, next)); + i = next; + } else { + i++; + } + } + int nalusTotalLen = + nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0; + if (nalus.isEmpty && data.isNotEmpty) { + nalus.add(data); + } else if (nalus.isNotEmpty && nalusTotalLen < data.length) { + nalus.add(data.sublist(nalusTotalLen)); + } + return nalus; } - // 输出综合性能分析 - AppLog.log("性能分析: 实际帧率=${actualFPS.toStringAsFixed(1)}fps, " + - "丢帧率=${dropRate.toStringAsFixed(1)}%, " + - "待处理帧数=$pendingFrames, " + - "处理中帧数=$processingFrames, " + - "状态=$performanceStatus"); + // 优化:I帧前只缓存SPS/PPS/IDR,首次写入严格顺序 + if (!_hasWrittenFirstIFrame) { + final nalus = splitNalus(frameData); + List> spsList = []; + List> ppsList = []; + List> idrList = []; + for (final nalu in nalus) { + int offset = 0; + if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) { + if (nalu[2] == 0x01) + offset = 3; + else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4; + } + if (nalu.length > offset) { + int naluType = nalu[offset] & 0x1F; + if (naluType == 7) { + spsList.add(nalu); + AppLog.log('SPS内容: ' + + nalu + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join(' ')); + } else if (naluType == 8) { + ppsList.add(nalu); + AppLog.log('PPS内容: ' + + nalu + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join(' ')); + } else if (naluType == 5) { + idrList.add(nalu); + } + // 其他类型不缓存也不写入头部 + } + } + // 只在首次I帧写入前缓存,严格顺序写入 + if (spsList.isNotEmpty && ppsList.isNotEmpty && idrList.isNotEmpty) { + for (final sps in spsList) { + await _writeSingleFrameToFile(_ensureStartCode(sps)); + AppLog.log('写入顺序: SPS'); + } + for (final pps in ppsList) { + await _writeSingleFrameToFile(_ensureStartCode(pps)); + AppLog.log('写入顺序: PPS'); + } + for (final idr in idrList) { + await _writeSingleFrameToFile(_ensureStartCode(idr)); + AppLog.log('写入顺序: IDR'); + } + _hasWrittenFirstIFrame = true; + } else { + // 未收齐SPS/PPS/IDR则继续缓存,等待下次I帧 + if (spsList.isNotEmpty) _preIFrameCache.addAll(spsList); + if (ppsList.isNotEmpty) _preIFrameCache.addAll(ppsList); + if (idrList.isNotEmpty) _preIFrameCache.addAll(idrList); + } + } else { + // 首帧后只写入IDR和P帧 + final nalus = splitNalus(frameData); + for (final nalu in nalus) { + int offset = 0; + if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) { + if (nalu[2] == 0x01) + offset = 3; + else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4; + } + if (nalu.length > offset) { + int naluType = nalu[offset] & 0x1F; + if (naluType == 5) { + await _writeSingleFrameToFile(_ensureStartCode(nalu)); + // AppLog.log('写入顺序: IDR'); + } else if (naluType == 1) { + await _writeSingleFrameToFile(_ensureStartCode(nalu)); + // AppLog.log('写入顺序: P帧'); + } else if (naluType == 7) { + // AppLog.log('遇到新SPS,已忽略'); + } else if (naluType == 8) { + // AppLog.log('遇到新PPS,已忽略'); + } + // 其他类型不写入 + } + } + } + } catch (e) { + AppLog.log('写入H264帧到文件失败: $e'); + } + } - // 重置统计数据 - state.lastPerformanceCheck = now; - state.lastFrameCount = state.renderedFrameCount.value; + // 统一NALU起始码为0x00000001 + List _ensureStartCode(List nalu) { + if (nalu.length >= 4 && + nalu[0] == 0x00 && + nalu[1] == 0x00 && + nalu[2] == 0x00 && + nalu[3] == 0x01) { + return nalu; + } else if (nalu.length >= 3 && + nalu[0] == 0x00 && + nalu[1] == 0x00 && + nalu[2] == 0x01) { + return [0x00, 0x00, 0x00, 0x01] + nalu.sublist(3); + } else { + return [0x00, 0x00, 0x00, 0x01] + nalu; + } + } + + /// 实际写入单帧到文件(带NALU头判断) + Future _writeSingleFrameToFile(List frameData) async { + bool hasNaluHeader = false; + if (frameData.length >= 4 && + frameData[0] == 0x00 && + frameData[1] == 0x00 && + ((frameData[2] == 0x01) || + (frameData[2] == 0x00 && frameData[3] == 0x01))) { + hasNaluHeader = true; + } + if (hasNaluHeader) { + await state.h264File!.writeAsBytes(frameData, mode: FileMode.append); + } else { + final List naluHeader = [0x00, 0x00, 0x01]; + await state.h264File! + .writeAsBytes(naluHeader + frameData, mode: FileMode.append); + } + } + + /// 初始化h264文件 + Future _initH264File() async { + try { + if (state.h264File != null) return; + // 获取Download目录 + Directory? downloadsDir; + if (Platform.isAndroid) { + // Android 10+ 推荐用getExternalStorageDirectory() + downloadsDir = await getExternalStorageDirectory(); + // 兼容部分ROM,优先用Download + final downloadPath = '/storage/emulated/0/Download'; + if (Directory(downloadPath).existsSync()) { + downloadsDir = Directory(downloadPath); + } + } else { + downloadsDir = await getApplicationDocumentsDirectory(); + } + final filePath = + '${downloadsDir!.path}/video_${DateTime.now().millisecondsSinceEpoch}.h264'; + state.h264FilePath = filePath; + state.h264File = File(filePath); + if (!await state.h264File!.exists()) { + await state.h264File!.create(recursive: true); + } + AppLog.log('H264文件初始化: $filePath'); + } catch (e) { + AppLog.log('H264文件初始化失败: $e'); + } + } + + /// 关闭h264文件 + Future _closeH264File() async { + try { + if (state.h264File != null) { + AppLog.log('H264文件已关闭: ${state.h264FilePath ?? ''}'); + } + state.h264File = null; + state.h264FilePath = null; + _preIFrameCache.clear(); + _hasWrittenFirstIFrame = false; + } catch (e) { + AppLog.log('关闭H264文件时出错: $e'); + } + } + + /// 从I帧数据中分割NALU并将SPS/PPS优先放入缓冲区(用于缓冲区发送) + void _extractAndBufferSpsPpsForBuffer( + List frameData, int durationMs, int frameSeq, int frameSeqI) { + List> splitNalus(List data) { + List> nalus = []; + int i = 0; + while (i < data.length - 3) { + int start = -1; + int next = -1; + if (data[i] == 0x00 && data[i + 1] == 0x00) { + if (data[i + 2] == 0x01) { + start = i; + i += 3; + } else if (i + 3 < data.length && + data[i + 2] == 0x00 && + data[i + 3] == 0x01) { + start = i; + i += 4; + } else { + i++; + continue; + } + next = i; + while (next < data.length - 3) { + if (data[next] == 0x00 && + data[next + 1] == 0x00 && + ((data[next + 2] == 0x01) || + (data[next + 2] == 0x00 && data[next + 3] == 0x01))) { + break; + } + next++; + } + nalus.add(data.sublist(start, next)); + i = next; + } else { + i++; + } + } + int nalusTotalLen = + nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0; + if (nalus.isEmpty && data.isNotEmpty) { + nalus.add(data); + } else if (nalus.isNotEmpty && nalusTotalLen < data.length) { + nalus.add(data.sublist(nalusTotalLen)); + } + return nalus; + } + + final nalus = splitNalus(frameData); + for (final nalu in nalus) { + int offset = 0; + if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) { + if (nalu[2] == 0x01) + offset = 3; + else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4; + } + if (nalu.length > offset) { + int naluType = nalu[offset] & 0x1F; + if (naluType == 7) { + // SPS + hasSps = true; + // 只在首次或内容变化时更新缓存 + if (spsCache == null || !_listEquals(spsCache!, nalu)) { + spsCache = List.from(nalu); + } + } else if (naluType == 8) { + // PPS + hasPps = true; + if (ppsCache == null || !_listEquals(ppsCache!, nalu)) { + ppsCache = List.from(nalu); + } + } + } + } + } + + // 新增:List比较工具 + bool _listEquals(List a, List b) { + if (a.length != b.length) return false; + for (int i = 0; i < a.length; i++) { + if (a[i] != b[i]) return false; + } + return true; + } + + // 新增:I帧处理方法 + void _handleIFrameWithSpsPpsAndIdr(List frameData, int durationMs, int frameSeq, int frameSeqI) { + // 清空缓冲区,丢弃I帧前所有未处理帧(只保留SPS/PPS/I帧) + state.h264FrameBuffer.clear(); + _extractAndBufferSpsPpsForBuffer(frameData, durationMs, frameSeq, frameSeqI); + // 只要缓存有SPS/PPS就先写入,再写I帧本体(只写IDR) + if (spsCache == null || ppsCache == null) { + // 没有SPS/PPS缓存,丢弃本次I帧 + return; + } + // 先写入SPS/PPS + _addFrameToBuffer(spsCache!, TalkDataH264Frame_FrameTypeE.I, durationMs, frameSeq, frameSeqI); + _addFrameToBuffer(ppsCache!, TalkDataH264Frame_FrameTypeE.I, durationMs, frameSeq, frameSeqI); + // 分割I帧包,只写入IDR(type 5) + List> nalus = []; + int i = 0; + List data = frameData; + while (i < data.length - 3) { + int start = -1; + int next = -1; + if (data[i] == 0x00 && data[i + 1] == 0x00) { + if (data[i + 2] == 0x01) { + start = i; + i += 3; + } else if (i + 3 < data.length && data[i + 2] == 0x00 && data[i + 3] == 0x01) { + start = i; + i += 4; + } else { + i++; + continue; + } + next = i; + while (next < data.length - 3) { + if (data[next] == 0x00 && data[next + 1] == 0x00 && ((data[next + 2] == 0x01) || (data[next + 2] == 0x00 && data[next + 3] == 0x01))) { + break; + } + next++; + } + nalus.add(data.sublist(start, next)); + i = next; + } else { + i++; + } + } + int nalusTotalLen = nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0; + if (nalus.isEmpty && data.isNotEmpty) { + nalus.add(data); + } else if (nalus.isNotEmpty && nalusTotalLen < data.length) { + nalus.add(data.sublist(nalusTotalLen)); + } + for (final nalu in nalus) { + int offset = 0; + if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) { + if (nalu[2] == 0x01) + offset = 3; + else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4; + } + if (nalu.length > offset) { + int naluType = nalu[offset] & 0x1F; + if (naluType == 5) { + _addFrameToBuffer(nalu, TalkDataH264Frame_FrameTypeE.I, durationMs, frameSeq, frameSeqI); + } + } + } + } + + // 新增:P帧处理方法 + void _handlePFrame(List frameData, int durationMs, int frameSeq, int frameSeqI) { + // 只写入P帧(type 1) + List> nalus = []; + int i = 0; + List data = frameData; + while (i < data.length - 3) { + int start = -1; + int next = -1; + if (data[i] == 0x00 && data[i + 1] == 0x00) { + if (data[i + 2] == 0x01) { + start = i; + i += 3; + } else if (i + 3 < data.length && data[i + 2] == 0x00 && data[i + 3] == 0x01) { + start = i; + i += 4; + } else { + i++; + continue; + } + next = i; + while (next < data.length - 3) { + if (data[next] == 0x00 && data[next + 1] == 0x00 && ((data[next + 2] == 0x01) || (data[next + 2] == 0x00 && data[next + 3] == 0x01))) { + break; + } + next++; + } + nalus.add(data.sublist(start, next)); + i = next; + } else { + i++; + } + } + int nalusTotalLen = nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0; + if (nalus.isEmpty && data.isNotEmpty) { + nalus.add(data); + } else if (nalus.isNotEmpty && nalusTotalLen < data.length) { + nalus.add(data.sublist(nalusTotalLen)); + } + for (final nalu in nalus) { + int offset = 0; + if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) { + if (nalu[2] == 0x01) + offset = 3; + else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4; + } + if (nalu.length > offset) { + int naluType = nalu[offset] & 0x1F; + if (naluType == 1) { + _addFrameToBuffer(nalu, TalkDataH264Frame_FrameTypeE.P, durationMs, frameSeq, frameSeqI); + } + } } } } diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_page.dart b/lib/talk/starChart/views/native/talk_view_native_decode_page.dart index 3172a6a6..a5c69b15 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_page.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_page.dart @@ -56,6 +56,7 @@ class _TalkViewNativeDecodePageState extends State state.animationController.forward(); } }); + } @override @@ -71,6 +72,7 @@ class _TalkViewNativeDecodePageState extends State child: Stack( alignment: Alignment.center, children: [ + // 悬浮帧率统计信息条 Obx( () { final double screenWidth = MediaQuery.of(context).size.width; @@ -93,32 +95,84 @@ class _TalkViewNativeDecodePageState extends State final double scaleWidth = physicalWidth / rotatedImageWidth; final double scaleHeight = physicalHeight / rotatedImageHeight; max(scaleWidth, scaleHeight); // 选择较大的缩放比例 - - return state.isLoading.isTrue - ? Image.asset( - 'images/main/monitorBg.png', - width: screenWidth, - height: screenHeight, - fit: BoxFit.cover, - ) - : PopScope( - canPop: false, - child: RepaintBoundary( - key: state.globalKey, - child: SizedBox.expand( - child: RotatedBox( - // 解码器不支持硬件旋转,使用RotatedBox - quarterTurns: -1, - child: Texture( - textureId: state.textureId.value!, - filterQuality: FilterQuality.medium, + return Column( + children: [ + Expanded( + child: state.isLoading.isTrue + ? Image.asset( + 'images/main/monitorBg.png', + width: screenWidth, + height: screenHeight, + fit: BoxFit.cover, + ) + : PopScope( + canPop: false, + child: RepaintBoundary( + key: state.globalKey, + child: SizedBox.expand( + child: RotatedBox( + // 解码器不支持硬件旋转,使用RotatedBox + quarterTurns: -1, + child: Texture( + textureId: state.textureId.value!, + filterQuality: FilterQuality.medium, + ), + ), + ), ), ), - ), - ), - ); + ), + ], + ); }, ), + Positioned( + top: 300.h, + right: 20.w, + child: Obx(() => Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.network_check, color: Colors.redAccent, size: 18), + SizedBox(width: 6), + Text( + '接受服务端H264帧率/秒: ', + style: TextStyle(color: Colors.white, fontSize: 15), + ), + Text( + '${state.networkH264Fps.value}', + style: TextStyle(color: Colors.redAccent, fontSize: 16, fontWeight: FontWeight.bold), + ), + Text(' fps', style: TextStyle(color: Colors.white, fontSize: 13)), + ], + ), + SizedBox(height: 4), + Row( + children: [ + Icon(Icons.send, color: Colors.blueAccent, size: 18), + SizedBox(width: 6), + Text( + '送入Native帧率/秒: ', + style: TextStyle(color: Colors.white, fontSize: 15), + ), + Text( + '${state.nativeSendFps.value}', + style: TextStyle(color: Colors.blueAccent, fontSize: 16, fontWeight: FontWeight.bold), + ), + Text(' fps', style: TextStyle(color: Colors.white, fontSize: 13)), + ], + ), + ], + ), + )), + ), Obx(() => state.isLoading.isTrue ? Positioned( bottom: 310.h, @@ -127,102 +181,6 @@ class _TalkViewNativeDecodePageState extends State style: TextStyle(color: Colors.black, fontSize: 26.sp), )) : Container()), - Obx(() => state.textureId.value != null && state.showFps.value - ? Positioned( - top: ScreenUtil().statusBarHeight + 10.h, - right: 20.w, - child: Container( - padding: - EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.h), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.5), - borderRadius: BorderRadius.circular(5.h), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - // Text( - // 'FPS: ${state.decoderFps.value.toStringAsFixed(1)}', - // style: TextStyle( - // color: _getPacketLossColor( - // state.packetLossRate.value), - // fontSize: 20.sp, - // ), - // ), - Text( - '丢包率: ${state.packetLossRate.value.toStringAsFixed(1)}%', - style: TextStyle( - color: _getPacketLossColor( - state.packetLossRate.value), - fontSize: 20.sp, - ), - ), - Text( - '消息丢失: ${state.messageLossRate.value.toStringAsFixed(1)}%', - style: TextStyle( - color: _getPacketLossColor( - state.messageLossRate.value), - fontSize: 20.sp, - ), - ), - Divider( - color: Colors.white30, - height: 10.h, - thickness: 1), - Text( - '已渲染帧: ${state.renderedFrameCount.value}', - style: - TextStyle(color: Colors.white, fontSize: 18.sp), - ), - Text( - '总帧数: ${state.totalFrames.value}', - style: - TextStyle(color: Colors.white, fontSize: 18.sp), - ), - Text( - '丢弃帧: ${state.droppedFrames.value}', - style: - TextStyle(color: Colors.white, fontSize: 18.sp), - ), - Text( - 'IDR帧: ${state.hasSentIDR.value ? "已发送" : "未发送"}', - style: TextStyle( - color: state.hasSentIDR.value - ? Colors.green - : Colors.red, - fontSize: 18.sp), - ), - Text( - 'SPS: ${state.hasSentSPS.value ? "已发送" : "未发送"}', - style: TextStyle( - color: state.hasSentSPS.value - ? Colors.green - : Colors.red, - fontSize: 18.sp), - ), - Text( - 'PPS: ${state.hasSentPPS.value ? "已发送" : "未发送"}', - style: TextStyle( - color: state.hasSentPPS.value - ? Colors.green - : Colors.red, - fontSize: 18.sp), - ), - Text( - 'keyFrameInterval: ${state.keyFrameInterval.value}', - style: - TextStyle(color: Colors.green, fontSize: 18.sp), - ), - Text( - 'decodingJitterMs: ${state.decodingJitterMs.value}', - style: - TextStyle(color: Colors.green, fontSize: 18.sp), - ), - ], - ), - ), - ) - : Container()), Obx(() => state.isLoading.isFalse && state.oneMinuteTime.value > 0 ? Positioned( top: ScreenUtil().statusBarHeight + 75.h, diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_state.dart b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart index d98d5d34..6eef401e 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_state.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart @@ -106,4 +106,19 @@ class TalkViewNativeDecodeState { // 帧跟踪Map,记录每个提交的帧,key为textureId_frameSeq Map> frameTracker = {}; + + // H264帧缓冲区相关 + final List> h264FrameBuffer = >[]; // H264帧缓冲区,存储帧数据和类型 + final int maxFrameBufferSize = 25; // 最大缓冲区大小 + final int targetFps = 120; // 目标解码帧率 + Timer? frameProcessTimer; // 帧处理定时器 + bool isProcessingFrame = false; // 是否正在处理帧 + int lastProcessedTimestamp = 0; // 上次处理帧的时间戳 + // H264文件保存相关 + String? h264FilePath; + File? h264File; + + // 新增:用于页面显示的帧率统计 + RxInt networkH264Fps = 0.obs; + RxInt nativeSendFps = 0.obs; } From 89eeb4f0b3944a57387dc40f68f84f8930666bf2 Mon Sep 17 00:00:00 2001 From: liyi Date: Wed, 30 Apr 2025 17:55:57 +0800 Subject: [PATCH 020/151] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0native=E8=A7=A3?= =?UTF-8?q?=E7=A0=81=E6=8F=92=E4=BB=B6=E6=94=AF=E6=8C=81720P=E5=AF=B9?= =?UTF-8?q?=E8=AE=B2=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/appRouters.dart | 4 +- .../starChart/constant/talk_constant.dart | 6 +- .../native/talk_view_native_decode_logic.dart | 270 +++++++----------- .../native/talk_view_native_decode_page.dart | 52 +--- .../native/talk_view_native_decode_state.dart | 8 +- 5 files changed, 115 insertions(+), 225 deletions(-) diff --git a/lib/appRouters.dart b/lib/appRouters.dart index 0e989312..776fedec 100755 --- a/lib/appRouters.dart +++ b/lib/appRouters.dart @@ -1198,7 +1198,7 @@ abstract class AppRouters { page: () => const DoubleLockLinkPage()), GetPage( name: Routers.starChartTalkView, page: () => const TalkViewPage()), - // GetPage(name: Routers.h264WebView, page: () => TalkViewNativeDecodePage()), // 插件播放页面 - GetPage(name: Routers.h264WebView, page: () => H264WebView()), // webview播放页面 + GetPage(name: Routers.h264WebView, page: () => TalkViewNativeDecodePage()), // 插件播放页面 + // GetPage(name: Routers.h264WebView, page: () => H264WebView()), // webview播放页面 ]; } diff --git a/lib/talk/starChart/constant/talk_constant.dart b/lib/talk/starChart/constant/talk_constant.dart index 60d57a0d..e6f72519 100644 --- a/lib/talk/starChart/constant/talk_constant.dart +++ b/lib/talk/starChart/constant/talk_constant.dart @@ -13,7 +13,11 @@ class TalkConstant { audioType: [AudioTypeE.G711], ); static TalkExpectReq H264Expect = TalkExpectReq( - videoType: [VideoTypeE.H264], + videoType: [VideoTypeE.H264_720P], + audioType: [AudioTypeE.G711], + ); + static TalkExpectReq H264_720P_Expect = TalkExpectReq( + videoType: [VideoTypeE.H264_720P], audioType: [AudioTypeE.G711], ); } diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart index 8129f204..568da186 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart @@ -60,25 +60,10 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { // 添加一个集合来跟踪已成功解码的I帧序号 final Set _decodedIFrames = {}; - // 定义一个变量来保存上一帧的时间戳 - int? _previousFrameTimestamp; - - int _flutterToNativeFrameCount = 0; - int _lastFlutterToNativePrintTime = 0; - int _networkFrameCount = 0; - int _lastNetworkPrintTime = 0; - Timer? _frameRefreshTimer; - - bool _isFrameAvailable = true; - int _renderedFrameCount = 0; - int _lastRenderedFrameTime = 0; - // 写入前的缓存队列(I帧前) final List> _preIFrameCache = []; bool _hasWrittenFirstIFrame = false; - bool _isStartNative = false; - // 新增:SPS/PPS状态追踪变量 bool hasSps = false; bool hasPps = false; @@ -87,35 +72,13 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { List? spsCache; List? ppsCache; - void _setupFrameRefresh() { - // 设置帧刷新定时器,16ms对应约60fps - _frameRefreshTimer = - Timer.periodic(const Duration(milliseconds: 16), (timer) { - if (_isFrameAvailable) { - _isFrameAvailable = false; - _renderedFrameCount++; - - // 每秒统计一次帧率 - int now = DateTime.now().millisecondsSinceEpoch; - if (now - _lastRenderedFrameTime > 1000) { - print('[Flutter] 每秒渲染帧数: $_renderedFrameCount'); - _renderedFrameCount = 0; - _lastRenderedFrameTime = now; - } - - // 请求Flutter重建widget - WidgetsBinding.instance.scheduleFrame(); - } - }); - } - - void onFrameAvailable() { - _isFrameAvailable = true; - } + // 新增:记录上一个已接收的frameSeq + int? _lastFrameSeq; // 初始化视频解码器 Future _initVideoDecoder() async { try { + state.isLoading.value = true; // 创建解码器配置 final config = VideoDecoderConfig( width: 1280, @@ -128,10 +91,15 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { if (textureId != null) { state.textureId.value = textureId; AppLog.log('视频解码器初始化成功:textureId=$textureId'); - // 启动帧处理定时器 + + VideoDecodePlugin.setOnFrameRenderedListener((textureId) { + state.isLoading.value = false; + AppLog.log('已经开始渲染======='); + }); } else { AppLog.log('视频解码器初始化失败'); } + // 启动定时器发送帧数据 _startFrameProcessTimer(); } catch (e) { AppLog.log('初始化视频解码器错误: $e'); @@ -178,15 +146,13 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { int frameSeq, int frameSeqI, ) { - _networkFrameCount++; - int now = DateTime.now().millisecondsSinceEpoch; - if (now - _lastNetworkPrintTime > 1000) { - AppLog.log('[Flutter] 每秒收到网络H264帧数: ' + _networkFrameCount.toString()); - state.networkH264Fps.value = _networkFrameCount; - _networkFrameCount = 0; - _lastNetworkPrintTime = now; + // 只允许frameSeq严格递增,乱序或重复帧直接丢弃 + if (_lastFrameSeq != null && frameSeq <= _lastFrameSeq!) { + // 可选:打印日志 + AppLog.log('丢弃乱序或重复帧: frameSeq=$frameSeq, lastFrameSeq=$_lastFrameSeq'); + return; } - + _lastFrameSeq = frameSeq; // 创建包含帧数据和类型的Map final Map frameMap = { 'frameData': frameData, @@ -203,15 +169,6 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { while (state.h264FrameBuffer.length > state.maxFrameBufferSize) { state.h264FrameBuffer.removeAt(0); } - - _flutterToNativeFrameCount++; - if (now - _lastFlutterToNativePrintTime > 1000) { - AppLog.log( - '[Flutter] 每秒送入Native帧数: ' + _flutterToNativeFrameCount.toString()); - state.nativeSendFps.value = _flutterToNativeFrameCount; - _flutterToNativeFrameCount = 0; - _lastFlutterToNativePrintTime = now; - } } /// 启动帧处理定时器 @@ -225,12 +182,8 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { // 创建新定时器 state.frameProcessTimer = Timer.periodic(Duration(milliseconds: intervalMs), (timer) { - if (state.isLoading.isTrue) { - state.isLoading.value = false; - } _processNextFrameFromBuffer(); }); - AppLog.log('启动帧处理定时器,目标帧率: ${state.targetFps}fps,间隔: ${intervalMs}ms'); } @@ -258,40 +211,28 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { final int frameSeqI = frameMap['frameSeqI']; int pts = DateTime.now().millisecondsSinceEpoch; - if (frameType == TalkDataH264Frame_FrameTypeE.P) { - // 以frameSeqI为I帧序号标识 - if (!(_decodedIFrames.contains(frameSeqI))) { - AppLog.log('丢弃P帧:未收到对应I帧,frameSeqI=${frameSeqI}'); - return; - } - } else if (frameType == TalkDataH264Frame_FrameTypeE.I) { - // 记录已解码I帧序号 - _decodedIFrames.add(frameSeq); - } + // if (frameType == TalkDataH264Frame_FrameTypeE.P) { + // // 以frameSeqI为I帧序号标识 + // if (!(_decodedIFrames.contains(frameSeqI))) { + // AppLog.log('丢弃P帧:未收到对应I帧,frameSeqI=${frameSeqI}'); + // return; + // } + // } else if (frameType == TalkDataH264Frame_FrameTypeE.I) { + // // 记录已解码I帧序号 + // _decodedIFrames.add(frameSeq); + // } // 实时写入h264文件 - _appendH264FrameToFile(frameData, frameType); + // _appendH264FrameToFile(frameData, frameType); final timestamp = DateTime.now().microsecondsSinceEpoch; - VideoDecodePlugin.decodeFrame( - frameData: Uint8List.fromList(frameData), - frameType: frameType == TalkDataH264Frame_FrameTypeE.I ? 1 : 0, + VideoDecodePlugin.sendFrame( + frameData: frameData, + frameType: frameType == TalkDataH264Frame_FrameTypeE.I ? 0 : 1, frameSeq: frameSeq, timestamp: timestamp, + splitNalFromIFrame: true, refIFrameSeq: frameSeqI, ); - - // 判断P帧对应I帧是否已解码,未解码则丢弃P帧 - if (frameType == TalkDataH264Frame_FrameTypeE.P) { - // 以frameSeqI为I帧序号标识 - if (!(_decodedIFrames.contains(frameSeqI))) { - AppLog.log('丢弃P帧:未收到对应I帧,frameSeqI=${frameSeqI}'); - } - } else if (frameType == TalkDataH264Frame_FrameTypeE.I) { - // 记录已解码I帧序号 - _decodedIFrames.add(frameSeq); - } - // 实时写入h264文件 - _appendH264FrameToFile(frameData, frameType); } catch (e) { AppLog.log('处理缓冲帧失败: $e'); } finally { @@ -306,7 +247,6 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { state.frameProcessTimer = null; state.h264FrameBuffer.clear(); state.isProcessingFrame = false; - // AppLog.log('停止帧处理定时器'); } // 发起接听命令 @@ -342,54 +282,35 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { _playAudioFrames(); break; case TalkData_ContentTypeE.H264: - // if (_isStartNative) { - // if (talkDataH264Frame != null) { - // // 如果是I帧,先分割NALU,找到SPS/PPS并优先放入缓冲区 - // if (talkDataH264Frame.frameType == - // TalkDataH264Frame_FrameTypeE.I) { - // // 清空缓冲区,丢弃I帧前所有未处理帧(只保留SPS/PPS/I帧) - // state.h264FrameBuffer.clear(); - // _extractAndBufferSpsPpsForBuffer( - // talkData.content, - // talkData.durationMs, - // talkDataH264Frame.frameSeq, - // talkDataH264Frame.frameSeqI); - // } - // _addFrameToBuffer( - // talkData.content, - // talkDataH264Frame.frameType, - // talkData.durationMs, - // talkDataH264Frame.frameSeq, - // talkDataH264Frame.frameSeqI); - // } - // } else { - // await VideoDecodePlugin.startNativePlayer( - // VideoDecoderConfig(width: 1280, height: 720, codecType: 'h264'), - // ); - // _isStartNative = true; - // } // 处理H264帧 if (state.textureId.value != null) { if (talkDataH264Frame != null) { - if (talkDataH264Frame.frameType == - TalkDataH264Frame_FrameTypeE.I) { - _handleIFrameWithSpsPpsAndIdr( - talkData.content, - talkData.durationMs, - talkDataH264Frame.frameSeq, - talkDataH264Frame.frameSeqI, - ); - return; - } else if (talkDataH264Frame.frameType == - TalkDataH264Frame_FrameTypeE.P) { - _handlePFrame( - talkData.content, - talkData.durationMs, - talkDataH264Frame.frameSeq, - talkDataH264Frame.frameSeqI, - ); - return; - } + _addFrameToBuffer( + talkData.content, + talkDataH264Frame.frameType, + talkData.durationMs, + talkDataH264Frame.frameSeq, + talkDataH264Frame.frameSeqI, + ); + // if (talkDataH264Frame.frameType == + // TalkDataH264Frame_FrameTypeE.I) { + // _handleIFrameWithSpsPpsAndIdr( + // talkData.content, + // talkData.durationMs, + // talkDataH264Frame.frameSeq, + // talkDataH264Frame.frameSeqI, + // ); + // return; + // } else if (talkDataH264Frame.frameType == + // TalkDataH264Frame_FrameTypeE.P) { + // _handlePFrame( + // talkData.content, + // talkData.durationMs, + // talkDataH264Frame.frameSeq, + // talkDataH264Frame.frameSeqI, + // ); + // return; + // } } } else { AppLog.log('无法处理H264帧:textureId为空'); @@ -459,7 +380,7 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { /// 播放音频数据 void _playAudioData(TalkData talkData) async { - if (state.isOpenVoice.value) { + if (state.isOpenVoice.value && state.isLoading.isFalse) { final list = G711().decodeAndDenoise(talkData.content, true, 8000, 300, 150); // // 将 PCM 数据转换为 PcmArrayInt16 @@ -545,9 +466,6 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { // 初始化音频播放器 _initFlutterPcmSound(); - // 启动播放定时器 - // _startPlayback(); - // 初始化录音控制器 _initAudioRecorder(); @@ -559,13 +477,11 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { // 初始化H264帧缓冲区 state.h264FrameBuffer.clear(); state.isProcessingFrame = false; - - _setupFrameRefresh(); } @override void onClose() { - _closeH264File(); + // _closeH264File(); // 停止帧处理定时器 _stopFrameProcessTimer(); @@ -603,8 +519,7 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { // 清空已解码I帧集合 _decodedIFrames.clear(); - _frameRefreshTimer?.cancel(); - _frameRefreshTimer = null; + super.onClose(); } @@ -894,16 +809,16 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { int naluType = nalu[offset] & 0x1F; if (naluType == 7) { spsList.add(nalu); - AppLog.log('SPS内容: ' + - nalu - .map((b) => b.toRadixString(16).padLeft(2, '0')) - .join(' ')); + // AppLog.log('SPS内容: ' + + // nalu + // .map((b) => b.toRadixString(16).padLeft(2, '0')) + // .join(' ')); } else if (naluType == 8) { ppsList.add(nalu); - AppLog.log('PPS内容: ' + - nalu - .map((b) => b.toRadixString(16).padLeft(2, '0')) - .join(' ')); + // AppLog.log('PPS内容: ' + + // nalu + // .map((b) => b.toRadixString(16).padLeft(2, '0')) + // .join(' ')); } else if (naluType == 5) { idrList.add(nalu); } @@ -914,15 +829,15 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { if (spsList.isNotEmpty && ppsList.isNotEmpty && idrList.isNotEmpty) { for (final sps in spsList) { await _writeSingleFrameToFile(_ensureStartCode(sps)); - AppLog.log('写入顺序: SPS'); + // AppLog.log('写入顺序: SPS'); } for (final pps in ppsList) { await _writeSingleFrameToFile(_ensureStartCode(pps)); - AppLog.log('写入顺序: PPS'); + // AppLog.log('写入顺序: PPS'); } for (final idr in idrList) { await _writeSingleFrameToFile(_ensureStartCode(idr)); - AppLog.log('写入顺序: IDR'); + // AppLog.log('写入顺序: IDR'); } _hasWrittenFirstIFrame = true; } else { @@ -1131,18 +1046,22 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { } // 新增:I帧处理方法 - void _handleIFrameWithSpsPpsAndIdr(List frameData, int durationMs, int frameSeq, int frameSeqI) { + void _handleIFrameWithSpsPpsAndIdr( + List frameData, int durationMs, int frameSeq, int frameSeqI) { // 清空缓冲区,丢弃I帧前所有未处理帧(只保留SPS/PPS/I帧) state.h264FrameBuffer.clear(); - _extractAndBufferSpsPpsForBuffer(frameData, durationMs, frameSeq, frameSeqI); + _extractAndBufferSpsPpsForBuffer( + frameData, durationMs, frameSeq, frameSeqI); // 只要缓存有SPS/PPS就先写入,再写I帧本体(只写IDR) if (spsCache == null || ppsCache == null) { // 没有SPS/PPS缓存,丢弃本次I帧 return; } // 先写入SPS/PPS - _addFrameToBuffer(spsCache!, TalkDataH264Frame_FrameTypeE.I, durationMs, frameSeq, frameSeqI); - _addFrameToBuffer(ppsCache!, TalkDataH264Frame_FrameTypeE.I, durationMs, frameSeq, frameSeqI); + _addFrameToBuffer(spsCache!, TalkDataH264Frame_FrameTypeE.I, durationMs, + frameSeq, frameSeqI); + _addFrameToBuffer(ppsCache!, TalkDataH264Frame_FrameTypeE.I, durationMs, + frameSeq, frameSeqI); // 分割I帧包,只写入IDR(type 5) List> nalus = []; int i = 0; @@ -1154,7 +1073,9 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { if (data[i + 2] == 0x01) { start = i; i += 3; - } else if (i + 3 < data.length && data[i + 2] == 0x00 && data[i + 3] == 0x01) { + } else if (i + 3 < data.length && + data[i + 2] == 0x00 && + data[i + 3] == 0x01) { start = i; i += 4; } else { @@ -1163,7 +1084,10 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { } next = i; while (next < data.length - 3) { - if (data[next] == 0x00 && data[next + 1] == 0x00 && ((data[next + 2] == 0x01) || (data[next + 2] == 0x00 && data[next + 3] == 0x01))) { + if (data[next] == 0x00 && + data[next + 1] == 0x00 && + ((data[next + 2] == 0x01) || + (data[next + 2] == 0x00 && data[next + 3] == 0x01))) { break; } next++; @@ -1174,7 +1098,8 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { i++; } } - int nalusTotalLen = nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0; + int nalusTotalLen = + nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0; if (nalus.isEmpty && data.isNotEmpty) { nalus.add(data); } else if (nalus.isNotEmpty && nalusTotalLen < data.length) { @@ -1190,14 +1115,16 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { if (nalu.length > offset) { int naluType = nalu[offset] & 0x1F; if (naluType == 5) { - _addFrameToBuffer(nalu, TalkDataH264Frame_FrameTypeE.I, durationMs, frameSeq, frameSeqI); + _addFrameToBuffer(nalu, TalkDataH264Frame_FrameTypeE.I, durationMs, + frameSeq, frameSeqI); } } } } // 新增:P帧处理方法 - void _handlePFrame(List frameData, int durationMs, int frameSeq, int frameSeqI) { + void _handlePFrame( + List frameData, int durationMs, int frameSeq, int frameSeqI) { // 只写入P帧(type 1) List> nalus = []; int i = 0; @@ -1209,7 +1136,9 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { if (data[i + 2] == 0x01) { start = i; i += 3; - } else if (i + 3 < data.length && data[i + 2] == 0x00 && data[i + 3] == 0x01) { + } else if (i + 3 < data.length && + data[i + 2] == 0x00 && + data[i + 3] == 0x01) { start = i; i += 4; } else { @@ -1218,7 +1147,10 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { } next = i; while (next < data.length - 3) { - if (data[next] == 0x00 && data[next + 1] == 0x00 && ((data[next + 2] == 0x01) || (data[next + 2] == 0x00 && data[next + 3] == 0x01))) { + if (data[next] == 0x00 && + data[next + 1] == 0x00 && + ((data[next + 2] == 0x01) || + (data[next + 2] == 0x00 && data[next + 3] == 0x01))) { break; } next++; @@ -1229,7 +1161,8 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { i++; } } - int nalusTotalLen = nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0; + int nalusTotalLen = + nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0; if (nalus.isEmpty && data.isNotEmpty) { nalus.add(data); } else if (nalus.isNotEmpty && nalusTotalLen < data.length) { @@ -1245,7 +1178,8 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { if (nalu.length > offset) { int naluType = nalu[offset] & 0x1F; if (naluType == 1) { - _addFrameToBuffer(nalu, TalkDataH264Frame_FrameTypeE.P, durationMs, frameSeq, frameSeqI); + _addFrameToBuffer(nalu, TalkDataH264Frame_FrameTypeE.P, durationMs, + frameSeq, frameSeqI); } } } diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_page.dart b/lib/talk/starChart/views/native/talk_view_native_decode_page.dart index a5c69b15..75af93b2 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_page.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_page.dart @@ -13,6 +13,7 @@ import 'package:star_lock/talk/call/callTalk.dart'; import 'package:star_lock/talk/starChart/constant/talk_status.dart'; import 'package:star_lock/talk/starChart/handle/impl/debug_Info_model.dart'; import 'package:star_lock/talk/starChart/handle/impl/udp_talk_data_handler.dart'; +import 'package:star_lock/talk/starChart/star_chart_manage.dart'; import 'package:star_lock/talk/starChart/views/native/talk_view_native_decode_logic.dart'; import 'package:star_lock/talk/starChart/views/native/talk_view_native_decode_state.dart'; import 'package:star_lock/talk/starChart/views/talkView/talk_view_logic.dart'; @@ -35,6 +36,7 @@ class _TalkViewNativeDecodePageState extends State final TalkViewNativeDecodeLogic logic = Get.put(TalkViewNativeDecodeLogic()); final TalkViewNativeDecodeState state = Get.find().state; + final startChartManage = StartChartManage(); @override void initState() { @@ -112,7 +114,7 @@ class _TalkViewNativeDecodePageState extends State child: SizedBox.expand( child: RotatedBox( // 解码器不支持硬件旋转,使用RotatedBox - quarterTurns: -1, + quarterTurns: startChartManage.rotateAngle ~/ 90, child: Texture( textureId: state.textureId.value!, filterQuality: FilterQuality.medium, @@ -126,53 +128,7 @@ class _TalkViewNativeDecodePageState extends State ); }, ), - Positioned( - top: 300.h, - right: 20.w, - child: Obx(() => Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.5), - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(Icons.network_check, color: Colors.redAccent, size: 18), - SizedBox(width: 6), - Text( - '接受服务端H264帧率/秒: ', - style: TextStyle(color: Colors.white, fontSize: 15), - ), - Text( - '${state.networkH264Fps.value}', - style: TextStyle(color: Colors.redAccent, fontSize: 16, fontWeight: FontWeight.bold), - ), - Text(' fps', style: TextStyle(color: Colors.white, fontSize: 13)), - ], - ), - SizedBox(height: 4), - Row( - children: [ - Icon(Icons.send, color: Colors.blueAccent, size: 18), - SizedBox(width: 6), - Text( - '送入Native帧率/秒: ', - style: TextStyle(color: Colors.white, fontSize: 15), - ), - Text( - '${state.nativeSendFps.value}', - style: TextStyle(color: Colors.blueAccent, fontSize: 16, fontWeight: FontWeight.bold), - ), - Text(' fps', style: TextStyle(color: Colors.white, fontSize: 13)), - ], - ), - ], - ), - )), - ), + Obx(() => state.isLoading.isTrue ? Positioned( bottom: 310.h, diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_state.dart b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart index 6eef401e..e3408141 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_state.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart @@ -109,16 +109,12 @@ class TalkViewNativeDecodeState { // H264帧缓冲区相关 final List> h264FrameBuffer = >[]; // H264帧缓冲区,存储帧数据和类型 - final int maxFrameBufferSize = 25; // 最大缓冲区大小 - final int targetFps = 120; // 目标解码帧率 + final int maxFrameBufferSize = 15; // 最大缓冲区大小 + final int targetFps = 25; // 目标解码帧率,只是为了快速填充native的缓冲区 Timer? frameProcessTimer; // 帧处理定时器 bool isProcessingFrame = false; // 是否正在处理帧 int lastProcessedTimestamp = 0; // 上次处理帧的时间戳 // H264文件保存相关 String? h264FilePath; File? h264File; - - // 新增:用于页面显示的帧率统计 - RxInt networkH264Fps = 0.obs; - RxInt nativeSendFps = 0.obs; } From e8e297d95be05c836ee5b74065beee9ccbcffaf9 Mon Sep 17 00:00:00 2001 From: liyi Date: Tue, 6 May 2025 11:42:16 +0800 Subject: [PATCH 021/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E4=B8=BA?= =?UTF-8?q?=E5=8E=9F=E6=9C=89=E7=9A=84864*480=E5=AF=B9=E8=AE=B2=E8=A7=86?= =?UTF-8?q?=E9=A2=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/talk/starChart/constant/talk_constant.dart | 2 +- .../views/native/talk_view_native_decode_logic.dart | 12 +++++++----- .../views/native/talk_view_native_decode_state.dart | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/talk/starChart/constant/talk_constant.dart b/lib/talk/starChart/constant/talk_constant.dart index e6f72519..ff8a6727 100644 --- a/lib/talk/starChart/constant/talk_constant.dart +++ b/lib/talk/starChart/constant/talk_constant.dart @@ -13,7 +13,7 @@ class TalkConstant { audioType: [AudioTypeE.G711], ); static TalkExpectReq H264Expect = TalkExpectReq( - videoType: [VideoTypeE.H264_720P], + videoType: [VideoTypeE.H264], audioType: [AudioTypeE.G711], ); static TalkExpectReq H264_720P_Expect = TalkExpectReq( diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart index 568da186..5aa2ea36 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart @@ -81,9 +81,9 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { state.isLoading.value = true; // 创建解码器配置 final config = VideoDecoderConfig( - width: 1280, + width: 864, // 实际视频宽度 - height: 720, + height: 480, codecType: 'h264', ); // 初始化解码器并获取textureId @@ -209,7 +209,8 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { final TalkDataH264Frame_FrameTypeE frameType = frameMap['frameType']; final int frameSeq = frameMap['frameSeq']; final int frameSeqI = frameMap['frameSeqI']; - int pts = DateTime.now().millisecondsSinceEpoch; + int pts = frameMap['pts']; + // int pts = DateTime.now().millisecondsSinceEpoch; // if (frameType == TalkDataH264Frame_FrameTypeE.P) { // // 以frameSeqI为I帧序号标识 @@ -224,12 +225,13 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { // 实时写入h264文件 // _appendH264FrameToFile(frameData, frameType); - final timestamp = DateTime.now().microsecondsSinceEpoch; + // final timestamp = DateTime.now().millisecondsSinceEpoch; + // final timestamp64 = timestamp is int ? timestamp : timestamp.toInt(); VideoDecodePlugin.sendFrame( frameData: frameData, frameType: frameType == TalkDataH264Frame_FrameTypeE.I ? 0 : 1, frameSeq: frameSeq, - timestamp: timestamp, + timestamp: pts, splitNalFromIFrame: true, refIFrameSeq: frameSeqI, ); diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_state.dart b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart index e3408141..9528671c 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_state.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart @@ -109,8 +109,8 @@ class TalkViewNativeDecodeState { // H264帧缓冲区相关 final List> h264FrameBuffer = >[]; // H264帧缓冲区,存储帧数据和类型 - final int maxFrameBufferSize = 15; // 最大缓冲区大小 - final int targetFps = 25; // 目标解码帧率,只是为了快速填充native的缓冲区 + final int maxFrameBufferSize = 30; // 最大缓冲区大小 + final int targetFps = 30; // 目标解码帧率,只是为了快速填充native的缓冲区 Timer? frameProcessTimer; // 帧处理定时器 bool isProcessingFrame = false; // 是否正在处理帧 int lastProcessedTimestamp = 0; // 上次处理帧的时间戳 From b84dbf4e62002d13943e6db6ec0c9e893f92f22c Mon Sep 17 00:00:00 2001 From: liyi Date: Tue, 6 May 2025 14:00:38 +0800 Subject: [PATCH 022/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E4=B8=BA?= =?UTF-8?q?=E5=8E=9F=E6=9C=89=E7=9A=84864*480=E5=AF=B9=E8=AE=B2=E8=A7=86?= =?UTF-8?q?=E9=A2=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../native/talk_view_native_decode_logic.dart | 21 +------------------ .../native/talk_view_native_decode_state.dart | 2 +- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart index 5aa2ea36..193c7379 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart @@ -209,7 +209,7 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { final TalkDataH264Frame_FrameTypeE frameType = frameMap['frameType']; final int frameSeq = frameMap['frameSeq']; final int frameSeqI = frameMap['frameSeqI']; - int pts = frameMap['pts']; + int pts = frameMap['pts']; // int pts = DateTime.now().millisecondsSinceEpoch; // if (frameType == TalkDataH264Frame_FrameTypeE.P) { @@ -294,25 +294,6 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { talkDataH264Frame.frameSeq, talkDataH264Frame.frameSeqI, ); - // if (talkDataH264Frame.frameType == - // TalkDataH264Frame_FrameTypeE.I) { - // _handleIFrameWithSpsPpsAndIdr( - // talkData.content, - // talkData.durationMs, - // talkDataH264Frame.frameSeq, - // talkDataH264Frame.frameSeqI, - // ); - // return; - // } else if (talkDataH264Frame.frameType == - // TalkDataH264Frame_FrameTypeE.P) { - // _handlePFrame( - // talkData.content, - // talkData.durationMs, - // talkDataH264Frame.frameSeq, - // talkDataH264Frame.frameSeqI, - // ); - // return; - // } } } else { AppLog.log('无法处理H264帧:textureId为空'); diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_state.dart b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart index 9528671c..d7039c52 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_state.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart @@ -109,7 +109,7 @@ class TalkViewNativeDecodeState { // H264帧缓冲区相关 final List> h264FrameBuffer = >[]; // H264帧缓冲区,存储帧数据和类型 - final int maxFrameBufferSize = 30; // 最大缓冲区大小 + final int maxFrameBufferSize = 17; // 最大缓冲区大小 final int targetFps = 30; // 目标解码帧率,只是为了快速填充native的缓冲区 Timer? frameProcessTimer; // 帧处理定时器 bool isProcessingFrame = false; // 是否正在处理帧 From 1e71efc2e528ecbc4803cb005507e011d6b9c972 Mon Sep 17 00:00:00 2001 From: liyi Date: Tue, 6 May 2025 16:43:28 +0800 Subject: [PATCH 023/151] =?UTF-8?q?fix:SKY=E7=8E=AF=E5=A2=83=E4=B8=8B?= =?UTF-8?q?=E4=B8=8D=E6=98=BE=E7=A4=BA=E8=AE=BE=E7=BD=AE=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=91=98=E5=BC=80=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../card/cardDetail/cardDetail_page.dart | 24 +++++++++++++------ .../fingerprintDetail_page.dart | 24 +++++++++++++------ .../passwordKeyDetail_page.dart | 3 ++- 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/lib/main/lockDetail/card/cardDetail/cardDetail_page.dart b/lib/main/lockDetail/card/cardDetail/cardDetail_page.dart index bc2fd663..8d8140e6 100755 --- a/lib/main/lockDetail/card/cardDetail/cardDetail_page.dart +++ b/lib/main/lockDetail/card/cardDetail/cardDetail_page.dart @@ -5,6 +5,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; +import 'package:star_lock/flavors.dart'; import 'package:star_lock/main/lockDetail/card/cardDetail/cardDetail_state.dart'; import '../../../../appRouters.dart'; @@ -179,13 +180,22 @@ class _CardDetailPageState extends State with RouteAware { isHaveRightWidget: true, rightWidget: SizedBox( width: 60.w, height: 50.h, child: _isStressFingerprint()))), - Obx(() => CommonItem( - leftTitel: '是否为管理员'.tr, - rightTitle: '', - isTipsImg: false, - isHaveRightWidget: true, - rightWidget: - SizedBox(width: 60.w, height: 50.h, child: _isAdmin()))), + Visibility( + visible: !F.isSKY, + child: Obx( + () => CommonItem( + leftTitel: '是否为管理员'.tr, + rightTitle: '', + isTipsImg: false, + isHaveRightWidget: true, + rightWidget: SizedBox( + width: 60.w, + height: 50.h, + child: _isAdmin(), + ), + ), + ), + ), Container(height: 10.h), CommonItem( leftTitel: '操作记录'.tr, diff --git a/lib/main/lockDetail/fingerprint/fingerprintDetail/fingerprintDetail_page.dart b/lib/main/lockDetail/fingerprint/fingerprintDetail/fingerprintDetail_page.dart index b3f66632..fc018b40 100755 --- a/lib/main/lockDetail/fingerprint/fingerprintDetail/fingerprintDetail_page.dart +++ b/lib/main/lockDetail/fingerprint/fingerprintDetail/fingerprintDetail_page.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; +import 'package:star_lock/flavors.dart'; import 'package:star_lock/main/lockDetail/fingerprint/fingerprintDetail/fingerprintDetail_state.dart'; import '../../../../appRouters.dart'; @@ -178,13 +179,22 @@ class _FingerprintDetailPageState extends State isHaveLine: true, rightWidget: SizedBox( width: 60.w, height: 50.h, child: _isStressFingerprint()))), - Obx(() => CommonItem( - leftTitel: '是否为管理员'.tr, - rightTitle: '', - isTipsImg: false, - isHaveRightWidget: true, - rightWidget: - SizedBox(width: 60.w, height: 50.h, child: _isAdmin()))), + Visibility( + visible: !F.isSKY, + child: Obx( + () => CommonItem( + leftTitel: '是否为管理员'.tr, + rightTitle: '', + isTipsImg: false, + isHaveRightWidget: true, + rightWidget: SizedBox( + width: 60.w, + height: 50.h, + child: _isAdmin(), + ), + ), + ), + ), Container(height: 10.h), CommonItem( leftTitel: '操作记录'.tr, diff --git a/lib/main/lockDetail/passwordKey/passwordKeyDetail/passwordKeyDetail_page.dart b/lib/main/lockDetail/passwordKey/passwordKeyDetail/passwordKeyDetail_page.dart index cc02af47..b9a39d6c 100755 --- a/lib/main/lockDetail/passwordKey/passwordKeyDetail/passwordKeyDetail_page.dart +++ b/lib/main/lockDetail/passwordKey/passwordKeyDetail/passwordKeyDetail_page.dart @@ -4,6 +4,7 @@ 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/flavors.dart'; import 'package:star_lock/main/lockDetail/passwordKey/passwordKeyDetail/passwordKeyDetail_logic.dart'; import 'package:star_lock/main/lockDetail/passwordKey/passwordKeyDetail/passwordKeyDetail_state.dart'; import 'package:star_lock/main/lockDetail/passwordKey/passwordKeyList/passwordKeyListEntity.dart'; @@ -133,7 +134,7 @@ class _PasswordKeyDetailPageState extends State action: () {}), Container(height: 10.h), Obx(() => Visibility( - visible: state.itemData.value.isCustom! == 1, + visible: state.itemData.value.isCustom! == 1 && !F.isSKY, child: Column( children: [ CommonItem( From de52becc21a4aa4229ab2463e7a439ece810ad76 Mon Sep 17 00:00:00 2001 From: liyi Date: Tue, 6 May 2025 16:43:47 +0800 Subject: [PATCH 024/151] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E8=83=81?= =?UTF-8?q?=E8=BF=AB=E9=80=9A=E7=9F=A5=E5=88=97=E8=A1=A8=E6=97=B6=E7=9A=84?= =?UTF-8?q?=E6=96=87=E6=9C=AC=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coerceOpenDoor/coerceOpenDoor/coerceOpenDoor_logic.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/main/lockDetail/messageWarn/msgNotification/coerceOpenDoor/coerceOpenDoor/coerceOpenDoor_logic.dart b/lib/main/lockDetail/messageWarn/msgNotification/coerceOpenDoor/coerceOpenDoor/coerceOpenDoor_logic.dart index e646ffb2..ff350d42 100755 --- a/lib/main/lockDetail/messageWarn/msgNotification/coerceOpenDoor/coerceOpenDoor/coerceOpenDoor_logic.dart +++ b/lib/main/lockDetail/messageWarn/msgNotification/coerceOpenDoor/coerceOpenDoor/coerceOpenDoor_logic.dart @@ -29,9 +29,9 @@ class CoerceOpenDoorLogic extends BaseGetXController { case 2: return '密码'.tr; case 3: - return '指纹'.tr; + return 'IC卡'.tr; case 4: - return '卡'.tr; + return '指纹'.tr; case 5: return '人脸'.tr; default: From 44cdd31b3d8ed908152b43ead3a9163021e18f18 Mon Sep 17 00:00:00 2001 From: liyi Date: Tue, 6 May 2025 16:44:02 +0800 Subject: [PATCH 025/151] =?UTF-8?q?fix:SKY=E7=8E=AF=E5=A2=83=E4=B8=8B?= =?UTF-8?q?=E4=B8=8D=E6=98=BE=E7=A4=BA=E8=AE=BE=E7=BD=AE=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=91=98=E5=BC=80=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../face/faceDetail/faceDetail_page.dart | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/lib/main/lockDetail/face/faceDetail/faceDetail_page.dart b/lib/main/lockDetail/face/faceDetail/faceDetail_page.dart index 766e81ab..e4af7a36 100755 --- a/lib/main/lockDetail/face/faceDetail/faceDetail_page.dart +++ b/lib/main/lockDetail/face/faceDetail/faceDetail_page.dart @@ -5,6 +5,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; +import 'package:star_lock/flavors.dart'; import 'package:star_lock/main/lockDetail/face/faceDetail/faceDetail_logic.dart'; import 'package:star_lock/main/lockDetail/face/faceDetail/faceDetail_state.dart'; @@ -174,13 +175,22 @@ class _FaceDetailPageState extends State with RouteAware { // isHaveRightWidget: true, // isHaveLine: true, // rightWidget: SizedBox(width: 60.w, height: 50.h, child: _isStressFace()))), - Obx(() => CommonItem( - leftTitel: '是否为管理员'.tr, - rightTitle: '', - isTipsImg: false, - isHaveRightWidget: true, - rightWidget: - SizedBox(width: 60.w, height: 50.h, child: _isAdmin()))), + Visibility( + visible: !F.isSKY, + child: Obx( + () => CommonItem( + leftTitel: '是否为管理员'.tr, + rightTitle: '', + isTipsImg: false, + isHaveRightWidget: true, + rightWidget: SizedBox( + width: 60.w, + height: 50.h, + child: _isAdmin(), + ), + ), + ), + ), Container(height: 10.h), CommonItem( leftTitel: '操作记录'.tr, From 3db24c3303036a80914918ed963884a6e242d5c7 Mon Sep 17 00:00:00 2001 From: liyi Date: Tue, 6 May 2025 16:44:21 +0800 Subject: [PATCH 026/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E7=8C=AB?= =?UTF-8?q?=E7=9C=BC=E7=9B=91=E6=8E=A7=E6=94=AF=E6=8C=81=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/main/lockDetail/lockDetail/lockDetail_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/main/lockDetail/lockDetail/lockDetail_page.dart b/lib/main/lockDetail/lockDetail/lockDetail_page.dart index ec09e9a3..ed3c9b68 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_page.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_page.dart @@ -1179,7 +1179,7 @@ class _LockDetailPageState extends State } //可视对讲门锁新增->监控 - if (state.keyInfos.value.lockFeature!.videoIntercom == 1) { + if (state.keyInfos.value.lockFeature!.isSupportCatEye == 1) { showWidgetArr.add( bottomItem('images/main/icon_catEyes.png', '监控'.tr, state.bottomBtnisEable.value, () async { From 4c47b65b0e39157c63666a7df3a11dff563b1619 Mon Sep 17 00:00:00 2001 From: liyi Date: Wed, 7 May 2025 15:06:57 +0800 Subject: [PATCH 027/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4ios=E7=AB=AF?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E7=99=BD=E8=BE=B9=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../native/talk_view_native_decode_page.dart | 61 ++++++++++--------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_page.dart b/lib/talk/starChart/views/native/talk_view_native_decode_page.dart index 75af93b2..b7c1fb12 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_page.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_page.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:math'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -58,7 +59,6 @@ class _TalkViewNativeDecodePageState extends State state.animationController.forward(); } }); - } @override @@ -97,35 +97,40 @@ class _TalkViewNativeDecodePageState extends State final double scaleWidth = physicalWidth / rotatedImageWidth; final double scaleHeight = physicalHeight / rotatedImageHeight; max(scaleWidth, scaleHeight); // 选择较大的缩放比例 - return Column( - children: [ - Expanded( - child: state.isLoading.isTrue - ? Image.asset( - 'images/main/monitorBg.png', - width: screenWidth, - height: screenHeight, - fit: BoxFit.cover, - ) - : PopScope( - canPop: false, - child: RepaintBoundary( - key: state.globalKey, - child: SizedBox.expand( - child: RotatedBox( - // 解码器不支持硬件旋转,使用RotatedBox - quarterTurns: startChartManage.rotateAngle ~/ 90, - child: Texture( - textureId: state.textureId.value!, - filterQuality: FilterQuality.medium, - ), - ), - ), + return state.isLoading.isTrue + ? Image.asset( + 'images/main/monitorBg.png', + width: screenWidth, + height: screenHeight, + fit: BoxFit.cover, + ) + : Positioned.fill( + child: PopScope( + canPop: false, + child: RepaintBoundary( + key: state.globalKey, + child: SizedBox.expand( + child: RotatedBox( + // 解码器不支持硬件旋转,使用RotatedBox + quarterTurns: + startChartManage.rotateAngle ~/ 90, + child: Platform.isIOS + ? Transform.scale( + scale: 1.008, // 轻微放大,消除iOS白边 + child: Texture( + textureId: state.textureId.value!, + filterQuality: FilterQuality.medium, + ), + ) + : Texture( + textureId: state.textureId.value!, + filterQuality: FilterQuality.medium, + ), ), ), - ), - ], - ); + ), + ), + ); }, ), From 557da4c5a18281b1fd0ab3d8fea7017506a27f7a Mon Sep 17 00:00:00 2001 From: liyi Date: Wed, 7 May 2025 15:25:14 +0800 Subject: [PATCH 028/151] =?UTF-8?q?fix:=E5=8F=96=E6=B6=88=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0=E7=8A=B6=E6=80=81=E6=A0=8F=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/talk/starChart/handle/impl/udp_talk_request_handler.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart index 3a26c1fa..55a2ac27 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart @@ -85,7 +85,7 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle //test:使用自定义铃声 playRingtone(); // 显示状态栏弹窗 - _showTalkRequestNotification(talkObjectName: talkObjectName); + // _showTalkRequestNotification(talkObjectName: talkObjectName); // 设置为等待接听状态 talkStatus.setPassiveCallWaitingAnswer(); if (startChartManage From ce6c02b73657bc3cee3cab086f89149c433cb78b Mon Sep 17 00:00:00 2001 From: liyi Date: Wed, 7 May 2025 16:32:57 +0800 Subject: [PATCH 029/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E8=A7=A3?= =?UTF-8?q?=E7=A0=81=E6=8F=92=E4=BB=B6=E4=BE=9D=E8=B5=96=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pubspec.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index a09deb7e..75fdbb3f 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -128,7 +128,9 @@ dependencies: aliyun_face_plugin: path: aliyun_face_plugin video_decode_plugin: - path: ../video_decode_plugin + git: + url: git@code.star-lock.cn:liyi/video_decode_plugin.git + ref: 73afceeedc74a3c987716c9956cbcc495d5650c3 flutter_localizations: sdk: flutter From 1217ea7d3ee63eef29244438c0075e5a2d49281c Mon Sep 17 00:00:00 2001 From: liyi Date: Thu, 8 May 2025 10:14:34 +0800 Subject: [PATCH 030/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E7=BC=93?= =?UTF-8?q?=E5=86=B2=E5=8C=BA=E5=A4=A7=E5=B0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../starChart/views/native/talk_view_native_decode_logic.dart | 2 +- .../starChart/views/native/talk_view_native_decode_state.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart index 193c7379..f416f98b 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart @@ -227,7 +227,7 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { // final timestamp = DateTime.now().millisecondsSinceEpoch; // final timestamp64 = timestamp is int ? timestamp : timestamp.toInt(); - VideoDecodePlugin.sendFrame( + await VideoDecodePlugin.sendFrame( frameData: frameData, frameType: frameType == TalkDataH264Frame_FrameTypeE.I ? 0 : 1, frameSeq: frameSeq, diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_state.dart b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart index d7039c52..8703012d 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_state.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart @@ -109,7 +109,7 @@ class TalkViewNativeDecodeState { // H264帧缓冲区相关 final List> h264FrameBuffer = >[]; // H264帧缓冲区,存储帧数据和类型 - final int maxFrameBufferSize = 17; // 最大缓冲区大小 + final int maxFrameBufferSize = 7; // 最大缓冲区大小 final int targetFps = 30; // 目标解码帧率,只是为了快速填充native的缓冲区 Timer? frameProcessTimer; // 帧处理定时器 bool isProcessingFrame = false; // 是否正在处理帧 From 34698971afe5a3c9fd48501f100c336869b03319 Mon Sep 17 00:00:00 2001 From: liyi Date: Thu, 8 May 2025 10:14:53 +0800 Subject: [PATCH 031/151] =?UTF-8?q?fix:=E6=8F=92=E4=BB=B6=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 75fdbb3f..e2178e20 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -130,7 +130,7 @@ dependencies: video_decode_plugin: git: url: git@code.star-lock.cn:liyi/video_decode_plugin.git - ref: 73afceeedc74a3c987716c9956cbcc495d5650c3 + ref: 38df1883f5108ec1ce590ba52318815333fded38 flutter_localizations: sdk: flutter From 07aa71c67998a0cc2145d2aa55aae579cddafb0d Mon Sep 17 00:00:00 2001 From: liyi Date: Tue, 22 Apr 2025 15:17:42 +0800 Subject: [PATCH 032/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E4=BC=98?= =?UTF-8?q?=E5=8C=96h264=E6=92=AD=E6=94=BE=E9=80=BB=E8=BE=91=E5=B9=B6?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E9=9F=B3=E9=A2=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lockDetail/lockDetail_logic.dart | 3 +- .../lockMian/entity/lockListInfo_entity.dart | 12 ++ .../handle/impl/udp_echo_test_handler.dart | 3 +- .../handle/impl/udp_talk_accept_handler.dart | 23 ++- .../handle/impl/udp_talk_data_handler.dart | 17 ++- .../handle/impl/udp_talk_expect_handler.dart | 4 +- .../handle/impl/udp_talk_request_handler.dart | 37 +++-- .../handle/other/h264_frame_handler.dart | 11 +- .../handle/other/talk_data_model.dart | 9 ++ .../handle/other/talk_data_repository.dart | 9 +- .../handle/scp_message_base_handle.dart | 6 +- lib/talk/starChart/star_chart_manage.dart | 37 ++--- .../views/talkView/talk_view_logic.dart | 27 +--- .../starChart/webView/h264_web_logic.dart | 133 +++++++++++++++--- .../webView/h264_web_view_state.dart | 3 + 15 files changed, 244 insertions(+), 90 deletions(-) create mode 100644 lib/talk/starChart/handle/other/talk_data_model.dart diff --git a/lib/main/lockDetail/lockDetail/lockDetail_logic.dart b/lib/main/lockDetail/lockDetail/lockDetail_logic.dart index f3668a2c..c03a2753 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_logic.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_logic.dart @@ -774,9 +774,10 @@ class LockDetailLogic extends BaseGetXController { showToast('设备未配网'.tr); return; } + // 发送监控id StartChartManage().startCallRequestMessageTimer( - ToPeerId: StartChartManage().lockPeerId ?? ''); + ToPeerId: StartChartManage().lockNetworkInfo.peerId ?? ''); } else { showToast('猫眼设置为省电模式时无法进行监控,请在猫眼设置中切换为其他模式'.tr); } diff --git a/lib/main/lockMian/entity/lockListInfo_entity.dart b/lib/main/lockMian/entity/lockListInfo_entity.dart index 754891c1..b98bb328 100755 --- a/lib/main/lockMian/entity/lockListInfo_entity.dart +++ b/lib/main/lockMian/entity/lockListInfo_entity.dart @@ -374,6 +374,9 @@ class LockFeature { this.isNoSupportedBlueBroadcast, this.wifiLockType, this.wifi, + this.isH264, + this.isH265, + this.isMJpeg, }); LockFeature.fromJson(Map json) { @@ -391,6 +394,9 @@ class LockFeature { isNoSupportedBlueBroadcast = json['isNoSupportedBlueBroadcast']; wifiLockType = json['wifiLockType']; wifi = json['wifi']; + isH264 = json['isH264']; + isH265 = json['isH265']; + isMJpeg = json['isMJpeg']; } int? password; @@ -407,6 +413,9 @@ class LockFeature { int? isNoSupportedBlueBroadcast; int? wifiLockType; int? wifi; + int? isH264; + int? isH265; + int? isMJpeg; Map toJson() { final Map data = {}; @@ -424,6 +433,9 @@ class LockFeature { data['isNoSupportedBlueBroadcast'] = isNoSupportedBlueBroadcast; data['wifiLockType'] = wifiLockType; data['wifi'] = wifi; + data['isH264'] = isH264; + data['isH265'] = isH265; + data['isMJpeg'] = isMJpeg; return data; } } diff --git a/lib/talk/starChart/handle/impl/udp_echo_test_handler.dart b/lib/talk/starChart/handle/impl/udp_echo_test_handler.dart index beebeb75..cff8154f 100644 --- a/lib/talk/starChart/handle/impl/udp_echo_test_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_echo_test_handler.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:get/get.dart'; import 'package:star_lock/talk/starChart/entity/scp_message.dart'; +import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart'; import 'package:star_lock/talk/starChart/handle/scp_message_base_handle.dart'; import 'package:star_lock/talk/starChart/handle/scp_message_handle.dart'; import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart'; @@ -22,7 +23,7 @@ class UdpEchoTestHandler extends ScpMessageBaseHandle EasyLoading.showToast(scpMessage.Payload, duration: 2000.milliseconds); } else { talkDataRepository.addTalkData( - TalkData(content: payload, contentType: TalkData_ContentTypeE.Image)); + TalkDataModel(talkData: TalkData(content: payload, contentType: TalkData_ContentTypeE.Image))); } } diff --git a/lib/talk/starChart/handle/impl/udp_talk_accept_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_accept_handler.dart index 5f71aafa..2605525c 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_accept_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_accept_handler.dart @@ -4,6 +4,7 @@ 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/main/lockMian/entity/lockListInfo_entity.dart'; import 'package:star_lock/talk/starChart/constant/message_type_constant.dart'; import 'package:star_lock/talk/starChart/constant/talk_status.dart'; import 'package:star_lock/talk/starChart/entity/scp_message.dart'; @@ -13,6 +14,7 @@ import 'package:star_lock/talk/starChart/proto/gateway_reset.pb.dart'; import 'package:star_lock/talk/starChart/proto/generic.pb.dart'; import 'package:star_lock/talk/starChart/proto/talk_accept.pb.dart'; import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart'; +import 'package:star_lock/tools/commonDataManage.dart'; import '../../star_chart_manage.dart'; @@ -77,7 +79,24 @@ class UdpTalkAcceptHandler extends ScpMessageBaseHandle } void _handleSendExpect() { - // 修改预期数据并启动发送预期数据定时器,在收到回复时停止 - startChartManage.sendImageVideoAndG711AudioTalkExpectData(); + final LockListInfoItemEntity currentKeyInfo = + CommonDataManage().currentKeyInfo; + final isH264 = currentKeyInfo.lockFeature?.isH264 == 1; + final isMJpeg = currentKeyInfo.lockFeature?.isMJpeg == 1; + + // 优先使用H264,其次是MJPEG + if (isH264) { + // 锁支持H264,发送H264视频和G711音频期望 + startChartManage.sendH264VideoAndG711AudioTalkExpectData(); + print('锁支持H264,发送H264视频格式期望数据'); + } else if (isMJpeg) { + // 锁只支持MJPEG,发送图像视频和G711音频期望 + startChartManage.sendImageVideoAndG711AudioTalkExpectData(); + print('锁不支持H264,支持MJPEG,发送MJPEG视频格式期望数据'); + } else { + // 默认使用图像视频 + startChartManage.sendImageVideoAndG711AudioTalkExpectData(); + print('锁不支持H264和MJPEG,默认发送图像视频格式期望数据'); + } } } diff --git a/lib/talk/starChart/handle/impl/udp_talk_data_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_data_handler.dart index 0a9e7ab3..44705624 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_data_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_data_handler.dart @@ -5,6 +5,7 @@ import 'package:star_lock/talk/starChart/constant/message_type_constant.dart'; import 'package:star_lock/talk/starChart/entity/scp_message.dart'; import 'package:star_lock/talk/starChart/handle/other/h264_frame_handler.dart'; import 'package:star_lock/talk/starChart/handle/other/packet_loss_statistics.dart'; +import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart'; import 'package:star_lock/talk/starChart/handle/scp_message_base_handle.dart'; import 'package:star_lock/talk/starChart/handle/scp_message_handle.dart'; import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart'; @@ -63,7 +64,7 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle int? messageId}) { // 获取统计信息 final stats = PacketLossStatistics().getStatistics(); - _asyncLog('丢包统计: $stats'); + // _asyncLog('丢包统计: $stats'); // _asyncLog( // '分包数据:messageId:$messageId [$spIndex/$spTotal] PayloadLength:$PayloadLength'); if (messageType == MessageTypeConstant.RealTimeData) { @@ -118,7 +119,7 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle void _handleVideoH264(TalkData talkData) { final TalkDataH264Frame talkDataH264Frame = TalkDataH264Frame(); talkDataH264Frame.mergeFromBuffer(talkData.content); - frameHandler.handleFrame(talkDataH264Frame); + frameHandler.handleFrame(talkDataH264Frame, talkData); } /// 处理图片数据 @@ -127,7 +128,11 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle await _processCompletePayload(Uint8List.fromList(talkData.content)); processCompletePayload.forEach((element) { talkData.content = element; - talkDataRepository.addTalkData(talkData); + talkDataRepository.addTalkData( + TalkDataModel( + talkData: talkData, + ), + ); }); } @@ -138,7 +143,11 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle // // 转pcm数据 // List pcmBytes = G711().convertList(g711Data); // talkData.content = pcmBytes; - talkDataRepository.addTalkData(talkData); + talkDataRepository.addTalkData( + TalkDataModel( + talkData: talkData, + ), + ); } catch (e) { print('Error decoding G.711 to PCM: $e'); } diff --git a/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart index 20dc1d2f..d1c83e77 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart @@ -20,7 +20,7 @@ import '../../star_chart_manage.dart'; class UdpTalkExpectHandler extends ScpMessageBaseHandle implements ScpMessageHandler { - final TalkViewState talkViewState = Get.put(TalkViewLogic()).state; + // final TalkViewState talkViewState = Get.put(TalkViewLogic()).state; @override void handleReq(ScpMessage scpMessage) { @@ -40,7 +40,7 @@ class UdpTalkExpectHandler extends ScpMessageBaseHandle startChartManage.stopTalkExpectMessageTimer(); // 停止发送对讲请求 startChartManage.stopCallRequestMessageTimer(); - talkViewState.rotateAngle.value = talkExpectResp.rotate ?? 0; + // talkViewState.rotateAngle.value = talkExpectResp.rotate ?? 0; // 收到预期数据的应答后,代表建立了连接,启动通话保持的监听 // 启动通话保持监听定时器(用来判断如果x秒内没有收到通话保持则执行的操作); talkePingOverTimeTimerManager.start(); diff --git a/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart index be2f4565..974c8f25 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart @@ -7,7 +7,9 @@ import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:get/get.dart'; import 'package:star_lock/appRouters.dart'; import 'package:star_lock/app_settings/app_settings.dart'; +import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart'; import 'package:star_lock/talk/starChart/constant/message_type_constant.dart'; +import 'package:star_lock/talk/starChart/constant/talk_constant.dart'; import 'package:star_lock/talk/starChart/constant/talk_status.dart'; import 'package:star_lock/talk/starChart/entity/scp_message.dart'; import 'package:star_lock/talk/starChart/handle/scp_message_base_handle.dart'; @@ -16,6 +18,7 @@ import 'package:star_lock/talk/starChart/proto/gateway_reset.pb.dart'; import 'package:star_lock/talk/starChart/proto/generic.pb.dart'; import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart'; import 'package:star_lock/talk/starChart/proto/talk_request.pb.dart'; +import 'package:star_lock/tools/commonDataManage.dart'; import 'package:star_lock/tools/push/xs_jPhush.dart'; import 'package:star_lock/tools/storage.dart'; import 'package:star_lock/translations/current_locale_tool.dart'; @@ -30,7 +33,6 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle @override void handleReq(ScpMessage scpMessage) async { - final currentTime = DateTime.now().millisecondsSinceEpoch; // 确保与上次请求间隔至少1秒 if (currentTime - _lastRequestTime < 1000) { @@ -105,18 +107,16 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle _showTalkRequestNotification(talkObjectName: talkObjectName); // 设置为等待接听状态 talkStatus.setPassiveCallWaitingAnswer(); - // 收到呼叫请求,跳转到接听页面 if (startChartManage - .getDefaultTalkExpect() - .videoType - .indexOf(VideoTypeE.H264) == - -1) { + .getDefaultTalkExpect() + .videoType + .contains(VideoTypeE.H264)) { Get.toNamed( - Routers.starChartTalkView, + Routers.h264WebView, ); } else { Get.toNamed( - Routers.h264WebView, + Routers.starChartTalkView, ); } } @@ -189,7 +189,24 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle } void _handleSendExpect() { - // 修改预期数据并启动发送预期数据定时器,在收到回复时停止 - startChartManage.sendOnlyImageVideoTalkExpectData(); + final LockListInfoItemEntity currentKeyInfo = + CommonDataManage().currentKeyInfo; + final isH264 = currentKeyInfo.lockFeature?.isH264 == 1; + final isMJpeg = currentKeyInfo.lockFeature?.isMJpeg == 1; + + // 优先使用H264,其次是MJPEG + if (isH264) { + // 锁支持H264,发送H264视频和G711音频期望 + startChartManage.sendOnlyH264VideoTalkExpectData(); + print('锁支持H264,发送H264视频格式期望数据'); + } else if (isMJpeg) { + // 锁只支持MJPEG,发送图像视频和G711音频期望 + startChartManage.sendOnlyImageVideoTalkExpectData(); + print('锁不支持H264,支持MJPEG,发送MJPEG视频格式期望数据'); + } else { + // 默认使用图像视频 + startChartManage.sendOnlyImageVideoTalkExpectData(); + print('锁不支持H264和MJPEG,默认发送图像视频格式期望数据'); + } } } diff --git a/lib/talk/starChart/handle/other/h264_frame_handler.dart b/lib/talk/starChart/handle/other/h264_frame_handler.dart index cd2f8bc2..903524da 100644 --- a/lib/talk/starChart/handle/other/h264_frame_handler.dart +++ b/lib/talk/starChart/handle/other/h264_frame_handler.dart @@ -3,15 +3,20 @@ import 'dart:typed_data'; import 'package:flutter/services.dart'; import 'package:star_lock/app_settings/app_settings.dart'; +import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart'; +import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart'; import '../../proto/talk_data_h264_frame.pb.dart'; class H264FrameHandler { + final void Function(TalkDataModel frameData) onCompleteFrame; - final void Function(List frameData) onCompleteFrame; + // 只记录最近一个I帧的序号 + int _lastProcessedIFrameSeq = -1; H264FrameHandler({required this.onCompleteFrame}); - void handleFrame(TalkDataH264Frame frame) { - onCompleteFrame(frame.frameData); + void handleFrame(TalkDataH264Frame frame, TalkData talkData) { + onCompleteFrame( + TalkDataModel(talkData: talkData, talkDataH264Frame: frame)); } } diff --git a/lib/talk/starChart/handle/other/talk_data_model.dart b/lib/talk/starChart/handle/other/talk_data_model.dart new file mode 100644 index 00000000..a84f1d3d --- /dev/null +++ b/lib/talk/starChart/handle/other/talk_data_model.dart @@ -0,0 +1,9 @@ +import 'package:star_lock/talk/starChart/proto/talk_data.pbserver.dart'; +import 'package:star_lock/talk/starChart/proto/talk_data_h264_frame.pb.dart'; + +class TalkDataModel { + TalkData? talkData; + TalkDataH264Frame? talkDataH264Frame; + + TalkDataModel({required this.talkData, this.talkDataH264Frame}); +} diff --git a/lib/talk/starChart/handle/other/talk_data_repository.dart b/lib/talk/starChart/handle/other/talk_data_repository.dart index 062835cb..587a481f 100644 --- a/lib/talk/starChart/handle/other/talk_data_repository.dart +++ b/lib/talk/starChart/handle/other/talk_data_repository.dart @@ -1,9 +1,10 @@ import 'dart:async'; +import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart'; import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart'; class TalkDataRepository { TalkDataRepository._() { - _talkDataStreamController = StreamController.broadcast( + _talkDataStreamController = StreamController.broadcast( onListen: () { _isListening = true; }, @@ -18,13 +19,13 @@ class TalkDataRepository { static TalkDataRepository get instance => _instance; - late final StreamController _talkDataStreamController; + late final StreamController _talkDataStreamController; bool _isListening = false; // 直接返回原始流,不做转换 - Stream get talkDataStream => _talkDataStreamController.stream; + Stream get talkDataStream => _talkDataStreamController.stream; - void addTalkData(TalkData talkData) { + void addTalkData(TalkDataModel talkData) { if (_isListening) { _talkDataStreamController.add(talkData); } diff --git a/lib/talk/starChart/handle/scp_message_base_handle.dart b/lib/talk/starChart/handle/scp_message_base_handle.dart index 11328f46..495db676 100644 --- a/lib/talk/starChart/handle/scp_message_base_handle.dart +++ b/lib/talk/starChart/handle/scp_message_base_handle.dart @@ -15,6 +15,7 @@ import 'package:star_lock/talk/starChart/constant/payload_type_constant.dart'; import 'package:star_lock/talk/starChart/constant/udp_constant.dart'; import 'package:star_lock/talk/starChart/entity/scp_message.dart'; import 'package:star_lock/talk/starChart/handle/other/h264_frame_handler.dart'; +import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart'; import 'package:star_lock/talk/starChart/handle/other/talk_data_repository.dart'; import 'package:star_lock/talk/starChart/handle/other/talke_data_over_time_timer_manager.dart'; @@ -55,10 +56,10 @@ class ScpMessageBaseHandle { // 处理出完整帧数据后的回调 final H264FrameHandler frameHandler = - H264FrameHandler(onCompleteFrame: (frameData) { + H264FrameHandler(onCompleteFrame: (TalkDataModel talkDataModel) { // 处理完整的帧数据 TalkDataRepository.instance.addTalkData( - TalkData(contentType: TalkData_ContentTypeE.H264, content: frameData), + talkDataModel, ); }); @@ -71,6 +72,7 @@ class ScpMessageBaseHandle { messageId: scpMessage.MessageId!, ); } + // 回复失败消息 void replyErrorMessage(ScpMessage scpMessage) { startChartManage.sendGenericRespErrorMessage( diff --git a/lib/talk/starChart/star_chart_manage.dart b/lib/talk/starChart/star_chart_manage.dart index 7ffed8e5..a8f9ad5f 100644 --- a/lib/talk/starChart/star_chart_manage.dart +++ b/lib/talk/starChart/star_chart_manage.dart @@ -113,7 +113,7 @@ class StartChartManage { final int _maxPayloadSize = 8 * 1024; // 分包大小 // 默认通话的期望数据格式 - TalkExpectReq _defaultTalkExpect = TalkConstant.ImageExpect; + TalkExpectReq _defaultTalkExpect = TalkConstant.H264Expect; String relayPeerId = ''; // 中继peerId @@ -227,20 +227,6 @@ class StartChartManage { /// 设置数据接收回调 _onReceiveData(_udpSocket!, Get.context!); - - // //ToDo: 增加对讲调试、正式可删除 - // // 每秒重置数据速率 - // Timer.periodic(Duration(seconds: 1), (Timer t) { - // UdpTalkDataHandler().resetDataRates(); - // // 更新调试信息 - // Provider.of(Get.context!, listen: false) - // .updateDebugInfo( - // UdpTalkDataHandler().getLastRecvDataRate() ~/ 1024, // 转换为KB - // UdpTalkDataHandler().getLastRecvPacketCount(), - // UdpTalkDataHandler().getLastSendDataRate() ~/ 1024, // 转换为KB - // UdpTalkDataHandler().getLastSendPacketCount(), - // ); - // }); }).catchError((error) { _log(text: 'Failed to bind UDP socket: $error'); }); @@ -1145,7 +1131,7 @@ class StartChartManage { } void reSetDefaultTalkExpect() { - _defaultTalkExpect = TalkConstant.ImageExpect; + _defaultTalkExpect = TalkConstant.H264Expect; } TalkExpectReq getDefaultTalkExpect() { @@ -1163,12 +1149,27 @@ class StartChartManage { } /// 修改预期接收到的数据 - void sendImageVideoAndG711AudioTalkExpectData() { - final talkExpectReq = TalkConstant.ImageExpect; + void sendOnlyH264VideoTalkExpectData() { + final talkExpectReq = TalkExpectReq( + videoType: [VideoTypeE.H264], + audioType: [], + ); changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer( talkExpect: talkExpectReq); } + /// 修改预期接收到的数据 + void sendImageVideoAndG711AudioTalkExpectData() { + changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer( + talkExpect: TalkConstant.ImageExpect); + } + + /// 修改预期接收到的数据 + void sendH264VideoAndG711AudioTalkExpectData() { + changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer( + talkExpect: TalkConstant.H264Expect); + } + /// 发送远程开锁 void sendRemoteUnLockMessage({ required String bluetoothDeviceName, diff --git a/lib/talk/starChart/views/talkView/talk_view_logic.dart b/lib/talk/starChart/views/talkView/talk_view_logic.dart index 779e44e9..a4a850ba 100644 --- a/lib/talk/starChart/views/talkView/talk_view_logic.dart +++ b/lib/talk/starChart/views/talkView/talk_view_logic.dart @@ -23,6 +23,7 @@ import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart'; import 'package:star_lock/network/api_repository.dart'; import 'package:star_lock/talk/call/g711.dart'; import 'package:star_lock/talk/starChart/constant/talk_status.dart'; +import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart'; import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart'; import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart'; import 'package:star_lock/talk/starChart/star_chart_manage.dart'; @@ -96,31 +97,15 @@ class TalkViewLogic extends BaseGetXController { // 监听音视频数据流 void _startListenTalkData() { - state.talkDataRepository.talkDataStream.listen((TalkData talkData) async { - final contentType = talkData.contentType; + state.talkDataRepository.talkDataStream + .listen((TalkDataModel talkDataModel) async { + final talkData = talkDataModel.talkData; + final contentType = talkData!.contentType; final currentTime = DateTime.now().millisecondsSinceEpoch; // 判断数据类型,进行分发处理 switch (contentType) { case TalkData_ContentTypeE.G711: - // // 第一帧到达时记录开始时间 - // if (_isFirstAudioFrame) { - // _startAudioTime = currentTime; - // _isFirstAudioFrame = false; - // } - - // 计算音频延迟 - final expectedTime = _startAudioTime + talkData.durationMs; - final audioDelay = currentTime - expectedTime; - - // 如果延迟太大,清空缓冲区并直接播放 - if (audioDelay > 500) { - state.audioBuffer.clear(); - if (state.isOpenVoice.value) { - _playAudioFrames(); - } - return; - } if (state.audioBuffer.length >= audioBufferSize) { state.audioBuffer.removeAt(0); // 丢弃最旧的数据 } @@ -195,7 +180,6 @@ class TalkViewLogic extends BaseGetXController { // _frameCount = 0; // _lastFpsUpdateTime = currentTime; // } - } else { // AppLog.log('⚠️ 帧未找到缓存 - Key: $cacheKey'); state.videoBuffer.removeAt(oldestIndex); // 移除无法播放的帧 @@ -497,7 +481,6 @@ class TalkViewLogic extends BaseGetXController { _initAudioRecorder(); requestPermissions(); - } @override diff --git a/lib/talk/starChart/webView/h264_web_logic.dart b/lib/talk/starChart/webView/h264_web_logic.dart index fdbaad3f..626dff04 100644 --- a/lib/talk/starChart/webView/h264_web_logic.dart +++ b/lib/talk/starChart/webView/h264_web_logic.dart @@ -25,7 +25,9 @@ import 'package:star_lock/network/api_repository.dart'; import 'package:star_lock/talk/call/g711.dart'; import 'package:star_lock/talk/starChart/constant/talk_status.dart'; import 'package:star_lock/talk/starChart/handle/other/packet_loss_statistics.dart'; +import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart'; import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart'; +import 'package:star_lock/talk/starChart/proto/talk_data_h264_frame.pb.dart'; import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart'; import 'package:star_lock/talk/starChart/star_chart_manage.dart'; import 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart'; @@ -44,6 +46,9 @@ class H264WebViewLogic extends BaseGetXController { // 添加模拟数据相关变量 static const int CHUNK_SIZE = 4096; Timer? _mockDataTimer; + int _startAudioTime = 0; // 开始播放时间戳 + int audioBufferSize = 2; // 音频默认缓冲2帧 + // 定义音频帧缓冲和发送函数 final List _bufferedAudioFrames = []; final Queue> _frameBuffer = Queue>(); @@ -51,7 +56,6 @@ class H264WebViewLogic extends BaseGetXController { @override void onInit() { - super.onInit(); // 初始化 WebView 控制器 state.webViewController = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) @@ -63,18 +67,40 @@ class H264WebViewLogic extends BaseGetXController { }, ); - state.isShowLoading.value = true; - // 加载本地 HTML - _loadLocalHtml(); + super.onInit(); // 创建流数据监听 _createFramesStreamListen(); - // playLocalTestVideo(); _startListenTalkStatus(); state.talkStatus.value = state.startChartTalkStatus.status; // 初始化音频播放器 _initFlutterPcmSound(); + // 初始化录音控制器 _initAudioRecorder(); + + // 加载本地 HTML + _loadLocalHtml(); + + // playLocalTestVideo(); + + requestPermissions(); + } + + Future requestPermissions() async { + // 申请存储权限 + var storageStatus = await Permission.storage.request(); + // 申请录音权限 + var microphoneStatus = await Permission.microphone.request(); + + if (storageStatus.isGranted && microphoneStatus.isGranted) { + print("Permissions granted"); + } else { + print("Permissions denied"); + // 如果权限被拒绝,可以提示用户或跳转到设置页面 + if (await Permission.storage.isPermanentlyDenied) { + openAppSettings(); // 跳转到应用设置页面 + } + } } /// 初始化音频录制器 @@ -96,16 +122,37 @@ class H264WebViewLogic extends BaseGetXController { } void _createFramesStreamListen() async { - state.talkDataRepository.talkDataStream.listen((TalkData event) async { - // 添加新帧到缓冲区 - _frameBuffer.add(event.content); + state.talkDataRepository.talkDataStream + .listen((TalkDataModel talkDataModel) async { + final talkData = talkDataModel.talkData; + final contentType = talkData!.contentType; + final currentTime = DateTime.now().millisecondsSinceEpoch; - // 当缓冲区超过最大容量时,发送最早的帧并移除 - while (_frameBuffer.length > FRAME_BUFFER_SIZE) { - if (_frameBuffer.isNotEmpty) { - final frame = _frameBuffer.removeFirst(); - await _sendBufferedData(frame); - } + // 判断数据类型,进行分发处理 + switch (contentType) { + case TalkData_ContentTypeE.G711: + if (state.audioBuffer.length >= audioBufferSize) { + state.audioBuffer.removeAt(0); // 丢弃最旧的数据 + } + state.audioBuffer.add(talkData); // 添加新数据 + // 添加音频播放逻辑,与视频类似 + _playAudioFrames(); + break; + case TalkData_ContentTypeE.H264: + // // 添加新帧到缓冲区 + _frameBuffer.add(talkData.content); + + // 当缓冲区超过最大容量时,发送最早的帧并移除 + while (_frameBuffer.length > FRAME_BUFFER_SIZE) { + if (_frameBuffer.isNotEmpty) { + final frame = _frameBuffer.removeFirst(); + await _sendBufferedData(frame); + } + if (state.isShowLoading.isTrue) { + state.isShowLoading.value = false; + } + } + break; } }); } @@ -134,6 +181,51 @@ class H264WebViewLogic extends BaseGetXController { // } // } + // 新增:音频帧播放逻辑 + void _playAudioFrames() { + // 如果缓冲区为空或未达到目标大小,不进行播放 + // 音频缓冲区要求更小,以减少延迟 + if (state.audioBuffer.isEmpty || + state.audioBuffer.length < audioBufferSize) { + return; + } + + // 找出时间戳最小的音频帧 + TalkData? oldestFrame; + int oldestIndex = -1; + for (int i = 0; i < state.audioBuffer.length; i++) { + if (oldestFrame == null || + state.audioBuffer[i].durationMs < oldestFrame.durationMs) { + oldestFrame = state.audioBuffer[i]; + oldestIndex = i; + } + } + + // 确保找到了有效帧 + if (oldestFrame != null && oldestIndex != -1) { + if (state.isOpenVoice.value) { + // 播放音频 + _playAudioData(oldestFrame); + } + state.audioBuffer.removeAt(oldestIndex); + } + } + + /// 播放音频数据 + void _playAudioData(TalkData talkData) async { + if (state.isOpenVoice.value) { + final list = + G711().decodeAndDenoise(talkData.content, true, 8000, 300, 150); + // // 将 PCM 数据转换为 PcmArrayInt16 + final PcmArrayInt16 fromList = PcmArrayInt16.fromList(list); + FlutterPcmSound.feed(fromList); + if (!state.isPlaying.value) { + FlutterPcmSound.play(); + state.isPlaying.value = true; + } + } + } + /// 加载html文件 Future _loadLocalHtml() async { // 加载 HTML 文件内容 @@ -186,10 +278,10 @@ class H264WebViewLogic extends BaseGetXController { Timer.periodic(const Duration(seconds: 1), (Timer t) { if (state.isShowLoading.isFalse) { state.oneMinuteTime.value++; - if (state.oneMinuteTime.value >= 60) { - t.cancel(); // 取消定时器 - state.oneMinuteTime.value = 0; - } + // if (state.oneMinuteTime.value >= 60) { + // t.cancel(); // 取消定时器 + // state.oneMinuteTime.value = 0; + // } } }); break; @@ -321,7 +413,7 @@ class H264WebViewLogic extends BaseGetXController { } // 首先应用固定增益提升基础音量 - List amplifiedFrame = _applyGain(frame, 1.6); + List amplifiedFrame = _applyGain(frame, 1.8); // 编码为G711数据 List encodedData = G711Tool.encode(amplifiedFrame, 0); // 0表示A-law _bufferedAudioFrames.addAll(encodedData); @@ -409,7 +501,7 @@ class H264WebViewLogic extends BaseGetXController { }); final LockSetInfoEntity lockSetInfoEntity = - await ApiRepository.to.getLockSettingInfoData( + await ApiRepository.to.getLockSettingInfoData( lockId: lockId.toString(), ); if (lockSetInfoEntity.errorCode!.codeIsSuccessful) { @@ -427,7 +519,6 @@ class H264WebViewLogic extends BaseGetXController { } } - @override void dispose() { // _mockDataTimer?.cancel(); diff --git a/lib/talk/starChart/webView/h264_web_view_state.dart b/lib/talk/starChart/webView/h264_web_view_state.dart index 2ae11041..671e805c 100644 --- a/lib/talk/starChart/webView/h264_web_view_state.dart +++ b/lib/talk/starChart/webView/h264_web_view_state.dart @@ -5,6 +5,7 @@ import 'package:flutter_voice_processor/flutter_voice_processor.dart'; import 'package:get/get.dart'; import 'package:star_lock/talk/starChart/constant/talk_status.dart'; import 'package:star_lock/talk/starChart/handle/other/talk_data_repository.dart'; +import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart'; import 'package:star_lock/talk/starChart/status/star_chart_talk_status.dart'; import 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart'; import 'package:webview_flutter/webview_flutter.dart'; @@ -49,4 +50,6 @@ class H264WebViewState { RxInt rotateAngle = 0.obs; // 旋转角度(以弧度为单位) RxBool hasAudioData = false.obs; // 是否有音频数据 RxInt lastAudioTimestamp = 0.obs; // 最后接收到的音频数据的时间戳 + List audioBuffer = [].obs; + RxBool isPlaying = false.obs; // 是否开始播放 } From 1784f75c475dde791943022bc67678cac9997b85 Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 25 Apr 2025 10:21:05 +0800 Subject: [PATCH 033/151] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0=E5=8E=9F?= =?UTF-8?q?=E7=94=9F=E6=8F=92=E4=BB=B6=E8=A7=A3=E7=A0=81=E7=9A=84=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E3=80=81=E5=A2=9E=E5=8A=A0h264=E3=80=81mjpeg=E5=88=87?= =?UTF-8?q?=E6=8D=A2=E7=9A=84debug=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/appRouters.dart | 2 + .../lockDetail/lockDetail_logic.dart | 19 +- .../lockDetail/lockDetail_page.dart | 58 +- .../lockDetail/lockDetail_state.dart | 11 +- lib/network/api_provider.dart | 2 +- lib/network/api_repository.dart | 2 +- .../handle/impl/udp_talk_data_handler.dart | 3 - .../handle/other/h264_frame_handler.dart | 3 - .../handle/other/packet_loss_statistics.dart | 84 +- lib/talk/starChart/star_chart_manage.dart | 2 +- .../native/talk_view_native_decode_logic.dart | 798 ++++++++++++++++++ .../native/talk_view_native_decode_page.dart | 557 ++++++++++++ .../native/talk_view_native_decode_state.dart | 109 +++ .../views/talkView/talk_view_logic.dart | 37 +- .../starChart/webView/h264_web_logic.dart | 20 +- pubspec.yaml | 3 +- 16 files changed, 1672 insertions(+), 38 deletions(-) create mode 100644 lib/talk/starChart/views/native/talk_view_native_decode_logic.dart create mode 100644 lib/talk/starChart/views/native/talk_view_native_decode_page.dart create mode 100644 lib/talk/starChart/views/native/talk_view_native_decode_state.dart diff --git a/lib/appRouters.dart b/lib/appRouters.dart index ab6c17a9..83b41650 100755 --- a/lib/appRouters.dart +++ b/lib/appRouters.dart @@ -60,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/starChart/views/native/talk_view_native_decode_page.dart'; import 'package:star_lock/talk/starChart/views/talkView/talk_view_page.dart'; import 'package:star_lock/talk/starChart/webView/h264_web_view.dart'; @@ -1184,6 +1185,7 @@ abstract class AppRouters { page: () => const DoubleLockLinkPage()), GetPage( name: Routers.starChartTalkView, page: () => const TalkViewPage()), + // GetPage(name: Routers.h264WebView, page: () => TalkViewNativeDecodePage()), GetPage(name: Routers.h264WebView, page: () => H264WebView()), ]; } diff --git a/lib/main/lockDetail/lockDetail/lockDetail_logic.dart b/lib/main/lockDetail/lockDetail/lockDetail_logic.dart index c03a2753..72683016 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_logic.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_logic.dart @@ -15,6 +15,8 @@ import 'package:star_lock/main/lockDetail/lockDetail/device_network_info.dart'; import 'package:star_lock/main/lockDetail/lockSet/lockTime/getServerDatetime_entity.dart'; import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart'; import 'package:star_lock/talk/starChart/constant/talk_status.dart'; +import 'package:star_lock/talk/starChart/handle/other/packet_loss_statistics.dart'; +import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart'; import 'package:star_lock/talk/starChart/star_chart_manage.dart'; import 'package:star_lock/tools/bugly/bugly_tool.dart'; import 'package:star_lock/tools/throttler.dart'; @@ -564,7 +566,7 @@ class LockDetailLogic extends BaseGetXController { // 获取手机联网token,根据锁设置里面获取的开锁时是否联网来判断是否调用这个接口 Future getLockNetToken() async { final LockNetTokenEntity entity = await ApiRepository.to - .getLockNetToken(lockId: state.keyInfos.value.lockId.toString()); + .getLockNetToken(lockId: state.keyInfos.value.lockId!); if (entity.errorCode!.codeIsSuccessful) { state.lockNetToken = entity.data!.token!.toString(); // AppLog.log('从服务器获取联网token:${state.lockNetToken}'); @@ -769,12 +771,12 @@ class LockDetailLogic extends BaseGetXController { if (catEyeConfig.isNotEmpty && catEyeConfig.length > 0 && catEyeConfig[0].catEyeMode != 0) { - if (StartChartManage().lockNetworkInfo.wifiName == null || - StartChartManage().lockNetworkInfo.wifiName == '') { + if ((StartChartManage().lockNetworkInfo.wifiName == null || + StartChartManage().lockNetworkInfo.wifiName == '') ) { showToast('设备未配网'.tr); return; } - + PacketLossStatistics().reset(); // 发送监控id StartChartManage().startCallRequestMessageTimer( ToPeerId: StartChartManage().lockNetworkInfo.peerId ?? ''); @@ -795,6 +797,15 @@ class LockDetailLogic extends BaseGetXController { @override void onInit() { super.onInit(); + + // 初始化开关状态为当前对讲视频模式 + final currentTalkExpect = StartChartManage().getDefaultTalkExpect(); + if (currentTalkExpect.videoType.contains(VideoTypeE.H264)) { + state.useH264Mode.value = true; + } else if (currentTalkExpect.videoType.contains(VideoTypeE.IMAGE)) { + state.useH264Mode.value = false; + } + state.LockSetChangeSetRefreshLockDetailWithTypeSubscription = eventBus .on() .listen((LockSetChangeSetRefreshLockDetailWithType event) { diff --git a/lib/main/lockDetail/lockDetail/lockDetail_page.dart b/lib/main/lockDetail/lockDetail/lockDetail_page.dart index 1040b5ba..93ea8f2f 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_page.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_page.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; @@ -88,7 +89,6 @@ class _LockDetailPageState extends State /// 路由订阅 AppRouteObserver().routeObserver.subscribe(this, ModalRoute.of(context)!); state.isOpenLockNeedOnline.refresh(); - } StreamSubscription? _lockRefreshLockDetailInfoDataEvent; @@ -507,6 +507,60 @@ class _LockDetailPageState extends State Widget skWidget() { return ListView( children: [ + // Container( + // padding: EdgeInsets.symmetric(vertical: 15, horizontal: 20), + // margin: EdgeInsets.only(top: 10, bottom: 10), + // decoration: BoxDecoration( + // color: Colors.white, + // borderRadius: BorderRadius.circular(10), + // boxShadow: [ + // BoxShadow( + // color: Colors.black.withOpacity(0.05), + // blurRadius: 5, + // offset: Offset(0, 2), + // ), + // ], + // ), + // child: Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // Text('对讲视频模式'.tr, + // style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + // Row( + // children: [ + // Text('mjpeg', + // style: TextStyle( + // fontSize: 14, + // color: !state.useH264Mode.value + // ? AppColors.mainColor + // : Colors.grey)), + // Obx(() => Switch( + // value: state.useH264Mode.value, + // activeColor: AppColors.mainColor, + // onChanged: (value) { + // state.useH264Mode.value = value; + // if (value) { + // // 使用H264模式 + // StartChartManage() + // .sendH264VideoAndG711AudioTalkExpectData(); + // } else { + // // 使用Image模式 + // StartChartManage() + // .sendImageVideoAndG711AudioTalkExpectData(); + // } + // }, + // )), + // Text('H264'.tr, + // style: TextStyle( + // fontSize: 14, + // color: state.useH264Mode.value + // ? AppColors.mainColor + // : Colors.grey)), + // ], + // ), + // ], + // ), + // ), Visibility( visible: (state.keyInfos.value.keyType == XSConstantMacro.keyTypeTime || @@ -1467,7 +1521,7 @@ class _LockDetailPageState extends State state.iSOpenLock.value = true; state.openLockBtnState.value = 1; state.animationController!.forward(); - // AppLog.log('点击开锁'); + AppLog.log('点击开锁'); if (isOpenLockNeedOnline) { // 不需要联网 state.openDoorModel = 0; diff --git a/lib/main/lockDetail/lockDetail/lockDetail_state.dart b/lib/main/lockDetail/lockDetail/lockDetail_state.dart index 01178f52..a949b693 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_state.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_state.dart @@ -7,18 +7,18 @@ import 'package:star_lock/main/lockDetail/lockSet/lockSet/lockSetInfo_entity.dar import '../../../blue/io_reply.dart'; import '../../lockMian/entity/lockListInfo_entity.dart'; - class LockDetailState { Rx keyInfos = LockListInfoItemEntity().obs; final Rx lockSetInfoData = LockSetInfoData().obs; late StreamSubscription replySubscription; - StreamSubscription? lockSetOpenOrCloseCheckInRefreshLockDetailWithAttendanceEvent; + StreamSubscription? + lockSetOpenOrCloseCheckInRefreshLockDetailWithAttendanceEvent; StreamSubscription? LockSetChangeSetRefreshLockDetailWithTypeSubscription; StreamSubscription? DetailLockInfo; StreamSubscription? SuccessfulDistributionNetworkEvent; String lockNetToken = '0'; - int differentialTime = 0;// 服务器时间与本地时间差值 + int differentialTime = 0; // 服务器时间与本地时间差值 bool isHaveNetwork = true; int lockUserNo = 0; int senderUserId = 0; @@ -41,7 +41,7 @@ class LockDetailState { RxBool bottomBtnisEable = true.obs; // 是否不可用 用于限制底部按钮是否可用 RxBool openDoorBtnisUneable = true.obs; // 当钥匙状态不能使用的情况下开锁按钮禁止使用,默认可用 - int openDoorModel = 0;// 离线开门0, 在线开门2 离线关门32 在线关门34 + int openDoorModel = 0; // 离线开门0, 在线开门2 离线关门32 在线关门34 //过渡动画控制器 AnimationController? animationController; @@ -58,6 +58,9 @@ class LockDetailState { int logCountPage = 10; // 蓝牙记录一页多少个 RxInt nextAuthTime = 0.obs; // 下次认证时间 + // 视频编码模式选择开关状态 + RxBool useH264Mode = true.obs; // true表示使用H264模式,false表示使用Image模式 + // LockDetailState() { // Map map = Get.arguments; // lockCount = map["lockCount"]; diff --git a/lib/network/api_provider.dart b/lib/network/api_provider.dart index 4cad97cc..01aabb37 100755 --- a/lib/network/api_provider.dart +++ b/lib/network/api_provider.dart @@ -353,7 +353,7 @@ class ApiProvider extends BaseProvider { ); // 获取手机联网token - Future getLockNetToken(String lockId) => post( + Future getLockNetToken(int lockId) => post( getLockNetTokenURL.toUrl, jsonEncode({ 'lockId': lockId, diff --git a/lib/network/api_repository.dart b/lib/network/api_repository.dart index b5e09c33..86d3c604 100755 --- a/lib/network/api_repository.dart +++ b/lib/network/api_repository.dart @@ -325,7 +325,7 @@ class ApiRepository { } // 获取手机联网token - Future getLockNetToken({required String lockId}) async { + Future getLockNetToken({required int lockId}) async { final res = await apiProvider.getLockNetToken(lockId); return LockNetTokenEntity.fromJson(res.body); } diff --git a/lib/talk/starChart/handle/impl/udp_talk_data_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_data_handler.dart index 44705624..028ffb73 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_data_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_data_handler.dart @@ -62,9 +62,6 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle int? spTotal, int? spIndex, int? messageId}) { - // 获取统计信息 - final stats = PacketLossStatistics().getStatistics(); - // _asyncLog('丢包统计: $stats'); // _asyncLog( // '分包数据:messageId:$messageId [$spIndex/$spTotal] PayloadLength:$PayloadLength'); if (messageType == MessageTypeConstant.RealTimeData) { diff --git a/lib/talk/starChart/handle/other/h264_frame_handler.dart b/lib/talk/starChart/handle/other/h264_frame_handler.dart index 903524da..face6b68 100644 --- a/lib/talk/starChart/handle/other/h264_frame_handler.dart +++ b/lib/talk/starChart/handle/other/h264_frame_handler.dart @@ -10,9 +10,6 @@ import '../../proto/talk_data_h264_frame.pb.dart'; class H264FrameHandler { final void Function(TalkDataModel frameData) onCompleteFrame; - // 只记录最近一个I帧的序号 - int _lastProcessedIFrameSeq = -1; - H264FrameHandler({required this.onCompleteFrame}); void handleFrame(TalkDataH264Frame frame, TalkData talkData) { diff --git a/lib/talk/starChart/handle/other/packet_loss_statistics.dart b/lib/talk/starChart/handle/other/packet_loss_statistics.dart index 34dd2282..8aabf6fd 100644 --- a/lib/talk/starChart/handle/other/packet_loss_statistics.dart +++ b/lib/talk/starChart/handle/other/packet_loss_statistics.dart @@ -10,6 +10,10 @@ class PacketLossStatistics { // key: messageId, value: {totalPackets, receivedPackets} final Map _packetsMap = HashMap(); + // 配置参数 + int _maxCapacity = 300; // 最大容量为300条记录 + int _timeoutMs = 30000; // 默认超时时间为30秒 + // 统计信息 int _totalMessages = 0; // 总消息数 int _lostMessages = 0; // 丢包的消息数 @@ -18,10 +22,19 @@ class PacketLossStatistics { // 记录分包数据 void recordPacket(int messageId, int currentIndex, int totalPackets) { + // 定期清理超时记录 + _cleanupExpiredPackets(); + + // 检查容量限制 + _checkCapacityLimit(); + if (!_packetsMap.containsKey(messageId)) { _packetsMap[messageId] = PacketInfo(totalPackets); _totalMessages++; _totalPackets += totalPackets; + } else { + // 更新时间戳 + _packetsMap[messageId]!.timestamp = DateTime.now().millisecondsSinceEpoch; } _packetsMap[messageId]!.receivedPackets.add(currentIndex); @@ -32,6 +45,51 @@ class PacketLossStatistics { } } + // 清理超时的记录 + void _cleanupExpiredPackets() { + final currentTime = DateTime.now().millisecondsSinceEpoch; + final expiredMessageIds = []; + + _packetsMap.forEach((messageId, info) { + // 如果记录超时,添加到待清理列表 + if (currentTime - info.timestamp > _timeoutMs) { + expiredMessageIds.add(messageId); + + // 统计丢包 + _lostMessages++; + _lostPackets += (info.totalPackets - info.receivedPackets.length); + } + }); + + // 移除超时记录 + for (var messageId in expiredMessageIds) { + _packetsMap.remove(messageId); + } + } + + // 检查并确保不超过最大容量 + void _checkCapacityLimit() { + if (_packetsMap.length <= _maxCapacity) { + return; + } + + // 如果超过容量限制,按时间戳排序并删除最旧的记录 + var entries = _packetsMap.entries.toList() + ..sort((a, b) => a.value.timestamp.compareTo(b.value.timestamp)); + + // 计算需要移除的数量(移除25%的旧记录,至少保证有一定空间) + int removeCount = (_packetsMap.length * 0.25).ceil(); + + // 移除并统计丢包 + for (int i = 0; i < removeCount && i < entries.length; i++) { + var entry = entries[i]; + _lostMessages++; + _lostPackets += + (entry.value.totalPackets - entry.value.receivedPackets.length); + _packetsMap.remove(entry.key); + } + } + // 检查丢包情况 void _checkPacketLoss(int messageId) { final info = _packetsMap[messageId]!; @@ -62,6 +120,28 @@ class PacketLossStatistics { return PacketLossInfo(messageLossRate, packetLossRate); } + // Getter和Setter,允许外部调整参数 + int get maxCapacity => _maxCapacity; + set maxCapacity(int value) { + if (value > 0) { + _maxCapacity = value; + // 设置新容量后立即检查 + _checkCapacityLimit(); + } + } + + int get timeoutMs => _timeoutMs; + set timeoutMs(int value) { + if (value > 0) { + _timeoutMs = value; + // 设置新超时后立即清理 + _cleanupExpiredPackets(); + } + } + + // 获取当前未完成记录数 + int get pendingRecordsCount => _packetsMap.length; + // 重置统计数据 void reset() { _packetsMap.clear(); @@ -76,8 +156,10 @@ class PacketLossStatistics { class PacketInfo { final int totalPackets; final Set receivedPackets = HashSet(); + int timestamp; // 添加时间戳字段,记录最后更新时间 - PacketInfo(this.totalPackets); + PacketInfo(this.totalPackets) + : timestamp = DateTime.now().millisecondsSinceEpoch; } // 丢包统计信息类 diff --git a/lib/talk/starChart/star_chart_manage.dart b/lib/talk/starChart/star_chart_manage.dart index a8f9ad5f..66de4aeb 100644 --- a/lib/talk/starChart/star_chart_manage.dart +++ b/lib/talk/starChart/star_chart_manage.dart @@ -1226,7 +1226,7 @@ class StartChartManage { await Storage.removerStarChartRegisterNodeInfo(); // 关闭udp服务 closeUdpSocket(); - PacketLossStatistics().reset(); + } /// 重置数据 diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart new file mode 100644 index 00000000..cf76414e --- /dev/null +++ b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart @@ -0,0 +1,798 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:ui' as ui; +import 'dart:math'; // Import the math package to use sqrt + +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.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:gallery_saver/gallery_saver.dart'; +import 'package:get/get.dart'; +import 'package:image_gallery_saver/image_gallery_saver.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:star_lock/app_settings/app_settings.dart'; +import 'package:star_lock/login/login/entity/LoginEntity.dart'; +import 'package:star_lock/main/lockDetail/lockDetail/lockDetail_logic.dart'; +import 'package:star_lock/main/lockDetail/lockDetail/lockDetail_state.dart'; +import 'package:star_lock/main/lockDetail/lockDetail/lockNetToken_entity.dart'; +import 'package:star_lock/main/lockDetail/lockSet/lockSet/lockSetInfo_entity.dart'; +import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart'; +import 'package:star_lock/network/api_repository.dart'; +import 'package:star_lock/talk/call/callTalk.dart'; +import 'package:star_lock/talk/call/g711.dart'; +import 'package:star_lock/talk/starChart/constant/talk_status.dart'; +import 'package:star_lock/talk/starChart/handle/other/packet_loss_statistics.dart'; +import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart'; +import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart'; +import 'package:star_lock/talk/starChart/proto/talk_data_h264_frame.pb.dart'; +import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart'; +import 'package:star_lock/talk/starChart/star_chart_manage.dart'; +import 'package:star_lock/talk/starChart/views/native/talk_view_native_decode_state.dart'; +import 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart'; +import 'package:star_lock/tools/G711Tool.dart'; +import 'package:star_lock/tools/bugly/bugly_tool.dart'; +import 'package:video_decode_plugin/video_decode_plugin.dart'; + +import '../../../../tools/baseGetXController.dart'; + +class TalkViewNativeDecodeLogic extends BaseGetXController { + final TalkViewNativeDecodeState state = TalkViewNativeDecodeState(); + + final LockDetailState lockDetailState = Get.put(LockDetailLogic()).state; + + int bufferSize = 25; // 初始化为默认大小 + + int audioBufferSize = 2; // 音频默认缓冲2帧 + + // 定义音频帧缓冲和发送函数 + final List _bufferedAudioFrames = []; + + // 添加监听状态和订阅引用 + bool _isListening = false; + StreamSubscription? _streamSubscription; + + Timer? _batchProcessTimer; + + // 添加一个集合来跟踪已成功解码的I帧序号 + final Set _decodedIFrames = {}; + + // 初始化视频解码器 + Future _initVideoDecoder() async { + try { + // 创建解码器配置 + final config = VideoDecoderConfig( + width: 864, + // 实际视频宽度 + height: 480, + frameRate: 25, + // 明确设置帧率 + // 增大缓冲区大小 + codecType: CodecType.h264, + // 编解码类型 + isDebug: true, + ); + + // 初始化解码器并获取textureId + final textureId = await VideoDecodePlugin.initDecoder(config); + + if (textureId != null) { + state.textureId.value = textureId; + AppLog.log('视频解码器初始化成功:textureId=$textureId'); + + // 设置帧回调 + VideoDecodePlugin.setFrameCallback(_onFrameAvailable); + + // 设置状态回调 + VideoDecodePlugin.setStateCallbackForTexture( + textureId, _onDecoderStateChanged); + + // 启动FPS监测 + startFpsMonitoring(); + } else { + AppLog.log('视频解码器初始化失败'); + } + } catch (e) { + AppLog.log('初始化视频解码器错误: $e'); + // 如果初始化失败,延迟后重试 + await Future.delayed(const Duration(seconds: 2)); + if (!Get.isRegistered()) { + return; // 如果控制器已经被销毁,不再重试 + } + _initVideoDecoder(); // 重试初始化 + } + } + + // 添加帧可用回调 + void _onFrameAvailable(int textureId) {} + + // 解码器状态变化回调 + void _onDecoderStateChanged( + int textureId, DecoderState decoderState, Map stats) { + String stateText; + switch (decoderState) { + case DecoderState.initializing: + state.isLoading.value = true; + stateText = "初始化中"; + break; + case DecoderState.ready: + stateText = "准备就绪"; + break; + case DecoderState.rendering: + stateText = "渲染中"; + state.isLoading.value = false; + break; + case DecoderState.error: + stateText = "出错"; + // 获取错误信息 + final errorMessage = stats['errorMessage'] as String?; + if (errorMessage != null) { + AppLog.log("解码器错误: $errorMessage"); + } + break; + case DecoderState.released: + stateText = "已释放"; + break; + default: + stateText = "未知状态"; + } + + // 更新统计信息 + if (stats.isNotEmpty) { + // 获取丢包率统计信息 + final PacketLossInfo packetLossInfo = + PacketLossStatistics().getStatistics(); + + // 更新FPS + // state.decoderFps.value = (stats['fps'] as num?)?.toDouble() ?? 0.0; + // 更新解码器详细信息 + state.renderedFrameCount.value = (stats['renderedFrames'] as int?) ?? 0; + state.totalFrames.value = (stats['totalFrames'] as int?) ?? 0; + state.droppedFrames.value = (stats['droppedFrames'] as int?) ?? 0; + state.hasSentIDR.value = (stats['hasSentIDR'] as bool?) ?? false; + state.hasSentSPS.value = (stats['hasSentSPS'] as bool?) ?? false; + state.hasSentPPS.value = (stats['hasSentPPS'] as bool?) ?? false; + state.keyFrameInterval.value = (stats['keyFrameInterval'] as int?) ?? 0; + state.decodingJitterMs.value = (stats['decodingJitterMs'] as int?) ?? 0; + + // 更新状态数据 + state.messageLossRate.value = packetLossInfo.messageLossRate; + state.packetLossRate.value = packetLossInfo.packetLossRate; + state.lastPacketStatsUpdateTime.value = + DateTime.now().millisecondsSinceEpoch; + } + } + + /// 初始化音频播放器 + void _initFlutterPcmSound() { + const int sampleRate = 8000; + FlutterPcmSound.setLogLevel(LogLevel.none); + FlutterPcmSound.setup(sampleRate: sampleRate, channelCount: 1); + // 设置 feed 阈值 + if (Platform.isAndroid) { + FlutterPcmSound.setFeedThreshold(1024); // Android 平台的特殊处理 + } else { + FlutterPcmSound.setFeedThreshold(2000); // 非 Android 平台的处理 + } + } + + /// 挂断 + void udpHangUpAction() async { + if (state.talkStatus.value == TalkStatus.answeredSuccessfully) { + // 如果是通话中就挂断 + StartChartManage().startTalkHangupMessageTimer(); + } else { + // 拒绝 + StartChartManage().startTalkRejectMessageTimer(); + } + if (state.textureId.value != null) { + VideoDecodePlugin.releaseDecoderForTexture(state.textureId.value!); + } + VideoDecodePlugin.releaseAllDecoders(); + Get.back(); + } + + // 发起接听命令 + void initiateAnswerCommand() { + StartChartManage().startTalkAcceptTimer(); + } + + // 监听音视频数据流 + void _startListenTalkData() { + // 防止重复监听 + if (_isListening) { + AppLog.log("已经存在数据流监听,避免重复监听"); + return; + } + + AppLog.log("==== 启动新的数据流监听 ===="); + _isListening = true; + + _streamSubscription = state.talkDataRepository.talkDataStream + .listen((TalkDataModel talkDataModel) async { + final talkData = talkDataModel.talkData; + final talkDataH264Frame = talkDataModel.talkDataH264Frame; + final contentType = talkData!.contentType; + + // 判断数据类型,进行分发处理 + switch (contentType) { + case TalkData_ContentTypeE.G711: + if (state.audioBuffer.length >= audioBufferSize) { + state.audioBuffer.removeAt(0); // 丢弃最旧的数据 + } + state.audioBuffer.add(talkData); // 添加新数据 + // 添加音频播放逻辑,与视频类似 + _playAudioFrames(); + break; + case TalkData_ContentTypeE.H264: + // 添加到视频帧缓冲区,而不是直接处理 + // _processH264Frame(talkData, talkDataH264Frame!); + // 直接处理H264视频帧 + _processH264Frame(talkData, talkDataH264Frame!); + + // 记录关键调试信息 + if (talkDataH264Frame!.frameType == TalkDataH264Frame_FrameTypeE.I) { + AppLog.log( + '帧序号${talkDataH264Frame.frameSeq};帧类型:${talkDataH264Frame.frameType.toString()};时间戳:${DateTime.now().millisecondsSinceEpoch}'); + } + break; + } + }); + } + + // 处理H264视频帧 + Future _processH264Frame( + TalkData talkData, TalkDataH264Frame frameInfo) async { + // 检查解码器是否已初始化 + if (state.textureId.value == null) { + // 可以记录日志或尝试初始化解码器 + AppLog.log('解码器尚未初始化,尝试重新初始化...'); + await _initVideoDecoder(); + + // 如果仍未初始化成功,则丢弃此帧 + if (state.textureId.value == null) { + return; + } + } + // 获取P帧对应的I帧序号 + final frameSeqI = frameInfo.frameSeqI; + + // P帧检查:如果依赖的I帧未解码成功,直接丢弃 + if (frameInfo.frameType == TalkDataH264Frame_FrameTypeE.P && + !_decodedIFrames.contains(frameSeqI)) { + AppLog.log('丢弃P帧: 依赖的I帧(${frameSeqI})尚未解码, P帧序号: ${frameInfo.frameSeq}'); + return; + } + + // 从talkData中提取H264帧数据 + final Uint8List frameData = Uint8List.fromList(talkData.content); + + // 确定帧类型 + final FrameType frameType = + frameInfo.frameType == TalkDataH264Frame_FrameTypeE.I + ? FrameType.iFrame + : FrameType.pFrame; + + // 将帧数据交给解码器处理 + try { + final bool result = + await VideoDecodePlugin.decodeFrame(frameData, frameType); + // 如果是I帧且成功解码,将其序号加入已解码I帧集合 + if (frameInfo.frameType == TalkDataH264Frame_FrameTypeE.I && result) { + _decodedIFrames.add(frameInfo.frameSeq); + // 限制集合大小,避免内存泄漏 + if (_decodedIFrames.length > 30) { + _decodedIFrames.remove(_decodedIFrames.first); + } + } + } catch (e) { + AppLog.log('解码帧错误: $e, 帧序号: ${frameInfo.frameSeq}'); + } + } + + // 新增:音频帧播放逻辑 + void _playAudioFrames() { + // 如果缓冲区为空或未达到目标大小,不进行播放 + // 音频缓冲区要求更小,以减少延迟 + if (state.audioBuffer.isEmpty || + state.audioBuffer.length < audioBufferSize) { + return; + } + + // 找出时间戳最小的音频帧 + TalkData? oldestFrame; + int oldestIndex = -1; + for (int i = 0; i < state.audioBuffer.length; i++) { + if (oldestFrame == null || + state.audioBuffer[i].durationMs < oldestFrame.durationMs) { + oldestFrame = state.audioBuffer[i]; + oldestIndex = i; + } + } + + // 确保找到了有效帧 + if (oldestFrame != null && oldestIndex != -1) { + if (state.isOpenVoice.value) { + // 播放音频 + _playAudioData(oldestFrame); + } + state.audioBuffer.removeAt(oldestIndex); + } + } + + /// 监听对讲状态 + 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; + case TalkStatus.answeredSuccessfully: + state.oneMinuteTimeTimer?.cancel(); // 取消旧定时器 + state.oneMinuteTimeTimer ??= + Timer.periodic(const Duration(seconds: 1), (Timer t) { + if (state.isLoading.isFalse) { + state.oneMinuteTime.value++; + } + }); + break; + default: + // 其他状态的处理 + break; + } + }); + } + + /// 播放音频数据 + void _playAudioData(TalkData talkData) async { + if (state.isOpenVoice.value) { + final list = + G711().decodeAndDenoise(talkData.content, true, 8000, 300, 150); + // // 将 PCM 数据转换为 PcmArrayInt16 + final PcmArrayInt16 fromList = PcmArrayInt16.fromList(list); + FlutterPcmSound.feed(fromList); + if (!state.isPlaying.value) { + FlutterPcmSound.play(); + state.isPlaying.value = true; + } + } + } + + /// 停止播放音频 + void _stopPlayG711Data() async { + await FlutterPcmSound.pause(); + await FlutterPcmSound.stop(); + await FlutterPcmSound.clear(); + } + + /// 获取权限状态 + 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(); + } + } + + Future requestPermissions() async { + // 申请存储权限 + var storageStatus = await Permission.storage.request(); + // 申请录音权限 + var microphoneStatus = await Permission.microphone.request(); + + if (storageStatus.isGranted && microphoneStatus.isGranted) { + print("Permissions granted"); + } else { + print("Permissions denied"); + // 如果权限被拒绝,可以提示用户或跳转到设置页面 + if (await Permission.storage.isPermanentlyDenied) { + openAppSettings(); // 跳转到应用设置页面 + } + } + } + + Future startRecording() async {} + + Future stopRecording() async {} + + @override + void onReady() { + super.onReady(); + } + + @override + void onInit() { + super.onInit(); + + // 启动监听音视频数据流 + _startListenTalkData(); + // 启动监听对讲状态 + _startListenTalkStatus(); + // 在没有监听成功之前赋值一遍状态 + // *** 由于页面会在状态变化之后才会初始化,导致识别不到最新的状态,在这里手动赋值 *** + state.talkStatus.value = state.startChartTalkStatus.status; + + // 初始化音频播放器 + _initFlutterPcmSound(); + + // 启动播放定时器 + // _startPlayback(); + + // 初始化录音控制器 + _initAudioRecorder(); + + requestPermissions(); + + // 初始化视频解码器 + _initVideoDecoder(); + } + + @override + void onClose() { + _stopPlayG711Data(); // 停止播放音频 + + state.audioBuffer.clear(); // 清空音频缓冲区 + + state.oneMinuteTimeTimer?.cancel(); + state.oneMinuteTimeTimer = null; + + // 停止播放音频 + stopProcessingAudio(); + + state.oneMinuteTimeTimer?.cancel(); // 取消旧定时器 + state.oneMinuteTimeTimer = null; // 取消旧定时器 + state.oneMinuteTime.value = 0; + + // 释放视频解码器资源 + if (state.textureId.value != null) { + VideoDecodePlugin.releaseDecoder(); + state.textureId.value = null; + } + + // 取消数据流监听 + _streamSubscription?.cancel(); + _isListening = false; + + // 停止FPS监测 + stopFpsMonitoring(); + // 重置期望数据 + StartChartManage().reSetDefaultTalkExpect(); + VideoDecodePlugin.releaseAllDecoders(); + + // 取消批处理定时器 + _batchProcessTimer?.cancel(); + _batchProcessTimer = null; + + // 清空已解码I帧集合 + _decodedIFrames.clear(); + + super.onClose(); + } + + /// 处理无效通话状态 + void _handleInvalidTalkStatus() { + // 停止播放音频 + _stopPlayG711Data(); + stopProcessingAudio(); + } + + /// 更新发送预期数据 + void updateTalkExpect() { + TalkExpectReq talkExpectReq = TalkExpectReq(); + state.isOpenVoice.value = !state.isOpenVoice.value; + if (!state.isOpenVoice.value) { + talkExpectReq = TalkExpectReq( + videoType: [VideoTypeE.IMAGE], + audioType: [], + ); + showToast('已静音'.tr); + } else { + talkExpectReq = TalkExpectReq( + videoType: [VideoTypeE.IMAGE], + audioType: [AudioTypeE.G711], + ); + } + + /// 修改发送预期数据 + StartChartManage().changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer( + talkExpect: talkExpectReq); + } + + /// 截图并保存到相册 + 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'); + showToast('截图已保存到相册'.tr); + } catch (e) { + AppLog.log('截图失败: $e'); + } + } + + // 远程开锁 + Future remoteOpenLock() async { + final lockPeerId = StartChartManage().lockPeerId; + final lockListPeerId = StartChartManage().lockListPeerId; + int lockId = lockDetailState.keyInfos.value.lockId ?? 0; + + // 如果锁列表获取到peerId,代表有多个锁,使用锁列表的peerId + // 从列表中遍历出对应的peerId + lockListPeerId.forEach((element) { + if (element.network?.peerId == lockPeerId) { + lockId = element.lockId ?? 0; + } + }); + + final LockSetInfoEntity lockSetInfoEntity = + await ApiRepository.to.getLockSettingInfoData( + lockId: lockId.toString(), + ); + if (lockSetInfoEntity.errorCode!.codeIsSuccessful) { + if (lockSetInfoEntity.data?.lockFeature?.remoteUnlock == 1 && + lockSetInfoEntity.data?.lockSettingInfo?.remoteUnlock == 1) { + final LoginEntity entity = await ApiRepository.to + .remoteOpenLock(lockId: lockId.toString(), timeOut: 60); + if (entity.errorCode!.codeIsSuccessful) { + showToast('已开锁'.tr); + StartChartManage().lockListPeerId = []; + } + } else { + showToast('该锁的远程开锁功能未启用'.tr); + } + } + } + + /// 初始化音频录制器 + void _initAudioRecorder() { + state.voiceProcessor = VoiceProcessor.instance; + } + + //开始录音 + Future startProcessingAudio() async { + try { + if (await state.voiceProcessor?.hasRecordAudioPermission() ?? false) { + await state.voiceProcessor?.start(state.frameLength, state.sampleRate); + final bool? isRecording = await state.voiceProcessor?.isRecording(); + state.isRecordingAudio.value = isRecording!; + state.startRecordingAudioTime.value = DateTime.now(); + + // 增加录音帧监听器和错误监听器 + state.voiceProcessor + ?.addFrameListeners([_onFrame]); + state.voiceProcessor?.addErrorListener(_onError); + } else { + // state.errorMessage.value = 'Recording permission not granted'; + } + } on PlatformException catch (ex) { + // state.errorMessage.value = 'Failed to start recorder: $ex'; + } + state.isOpenVoice.value = false; + } + + /// 停止录音 + Future stopProcessingAudio() async { + try { + await state.voiceProcessor?.stop(); + state.voiceProcessor?.removeFrameListener(_onFrame); + state.udpSendDataFrameNumber = 0; + // 记录结束时间 + state.endRecordingAudioTime.value = DateTime.now(); + + // 计算录音的持续时间 + final Duration duration = state.endRecordingAudioTime.value + .difference(state.startRecordingAudioTime.value); + + state.recordingAudioTime.value = duration.inSeconds; + } on PlatformException catch (ex) { + // state.errorMessage.value = 'Failed to stop recorder: $ex'; + } finally { + final bool? isRecording = await state.voiceProcessor?.isRecording(); + state.isRecordingAudio.value = isRecording!; + state.isOpenVoice.value = true; + } + } + +// 音频帧处理 + Future _onFrame(List frame) async { + // 添加最大缓冲限制 + if (_bufferedAudioFrames.length > state.frameLength * 3) { + _bufferedAudioFrames.clear(); // 清空过多积累的数据 + return; + } + + // 首先应用固定增益提升基础音量 + List amplifiedFrame = _applyGain(frame, 1.6); + // 编码为G711数据 + List encodedData = G711Tool.encode(amplifiedFrame, 0); // 0表示A-law + _bufferedAudioFrames.addAll(encodedData); + // 使用相对时间戳 + final int ms = DateTime.now().millisecondsSinceEpoch % 1000000; // 使用循环时间戳 + int getFrameLength = state.frameLength; + if (Platform.isIOS) { + getFrameLength = state.frameLength * 2; + } + + // 添加发送间隔控制 + if (_bufferedAudioFrames.length >= state.frameLength) { + try { + await StartChartManage().sendTalkDataMessage( + talkData: TalkData( + content: _bufferedAudioFrames, + contentType: TalkData_ContentTypeE.G711, + durationMs: ms, + ), + ); + } finally { + _bufferedAudioFrames.clear(); // 确保清理缓冲区 + } + } else { + _bufferedAudioFrames.addAll(encodedData); + } + } + +// 错误监听 + void _onError(VoiceProcessorException error) { + AppLog.log(error.message!); + } + +// 添加音频增益处理方法 + List _applyGain(List pcmData, double gainFactor) { + List result = List.filled(pcmData.length, 0); + + for (int i = 0; i < pcmData.length; i++) { + // PCM数据通常是有符号的16位整数 + int sample = pcmData[i]; + + // 应用增益 + double amplified = sample * gainFactor; + + // 限制在有效范围内,防止溢出 + if (amplified > 32767) { + amplified = 32767; + } else if (amplified < -32768) { + amplified = -32768; + } + + result[i] = amplified.toInt(); + } + + return result; + } + + // 启动网络状态监测 + void startFpsMonitoring() { + // 确保只有一个计时器在运行 + stopFpsMonitoring(); + + // 初始化时间记录 + state.lastFpsUpdateTime.value = DateTime.now().millisecondsSinceEpoch; + + // 创建一个计时器,每秒更新一次丢包率和性能数据 + state.fpsTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + // 更新丢包率数据 + updatePacketLossStats(); + + // 分析性能数据 + _analyzePerformance(); + }); + } + + // 停止网络状态监测 + void stopFpsMonitoring() { + state.fpsTimer?.cancel(); + state.fpsTimer = null; + } + + // 日志记录方法 + void logMessage(String message) { + AppLog.log(message); + } + + // 更新丢包率统计数据 + void updatePacketLossStats() async { + try {} catch (e) { + logMessage('获取丢包率数据失败: $e'); + } + } + + // 添加性能分析方法 + void _analyzePerformance() { + final int now = DateTime.now().millisecondsSinceEpoch; + + // 如果是首次调用,初始化数据 + if (state.lastPerformanceCheck == 0) { + state.lastPerformanceCheck = now; + state.lastFrameCount = state.renderedFrameCount.value; + return; + } + + // 每秒分析一次性能 + if (now - state.lastPerformanceCheck >= 1000) { + // 计算过去一秒的实际帧率 + final int frameRendered = + state.renderedFrameCount.value - state.lastFrameCount; + final double actualFPS = + frameRendered * 1000 / (now - state.lastPerformanceCheck); + + // 计算丢帧率 + final double dropRate = state.droppedFrames.value / + (state.totalFrames.value > 0 ? state.totalFrames.value : 1) * + 100; + + // 计算当前解码器积压帧数 + final int pendingFrames = + state.totalFrames.value - state.renderedFrameCount.value; + + // 计算跟踪Map中的帧数(正在处理中的帧) + final int processingFrames = state.frameTracker.length; + + // 分析渲染瓶颈 + String performanceStatus = "正常"; + if (actualFPS < 15 && dropRate > 10) { + performanceStatus = "严重渲染瓶颈"; + } else if (actualFPS < 20 && dropRate > 5) { + performanceStatus = "轻微渲染瓶颈"; + } + + // 输出综合性能分析 + AppLog.log("性能分析: 实际帧率=${actualFPS.toStringAsFixed(1)}fps, " + + "丢帧率=${dropRate.toStringAsFixed(1)}%, " + + "待处理帧数=$pendingFrames, " + + "处理中帧数=$processingFrames, " + + "状态=$performanceStatus"); + + // 重置统计数据 + state.lastPerformanceCheck = now; + state.lastFrameCount = state.renderedFrameCount.value; + } + } +} diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_page.dart b/lib/talk/starChart/views/native/talk_view_native_decode_page.dart new file mode 100644 index 00000000..3172a6a6 --- /dev/null +++ b/lib/talk/starChart/views/native/talk_view_native_decode_page.dart @@ -0,0 +1,557 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:http/http.dart' as http; +import 'package:provider/provider.dart'; +import 'package:star_lock/flavors.dart'; +import 'package:star_lock/talk/call/callTalk.dart'; +import 'package:star_lock/talk/starChart/constant/talk_status.dart'; +import 'package:star_lock/talk/starChart/handle/impl/debug_Info_model.dart'; +import 'package:star_lock/talk/starChart/handle/impl/udp_talk_data_handler.dart'; +import 'package:star_lock/talk/starChart/views/native/talk_view_native_decode_logic.dart'; +import 'package:star_lock/talk/starChart/views/native/talk_view_native_decode_state.dart'; +import 'package:star_lock/talk/starChart/views/talkView/talk_view_logic.dart'; +import 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart'; +import 'package:video_decode_plugin/video_decode_plugin.dart'; + +import '../../../../app_settings/app_colors.dart'; +import '../../../../tools/showTFView.dart'; + +class TalkViewNativeDecodePage extends StatefulWidget { + const TalkViewNativeDecodePage({Key? key}) : super(key: key); + + @override + State createState() => + _TalkViewNativeDecodePageState(); +} + +class _TalkViewNativeDecodePageState extends State + with TickerProviderStateMixin { + final TalkViewNativeDecodeLogic logic = Get.put(TalkViewNativeDecodeLogic()); + final TalkViewNativeDecodeState state = + Get.find().state; + + @override + void initState() { + super.initState(); + + state.animationController = AnimationController( + vsync: this, // 确保使用的TickerProvider是当前Widget + duration: const Duration(seconds: 1), + ); + + state.animationController.repeat(); + //动画开始、结束、向前移动或向后移动时会调用StatusListener + state.animationController.addStatusListener((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 WillPopScope( + onWillPop: () async { + // 返回 false 表示禁止退出 + return false; + }, + child: SizedBox( + width: 1.sw, + height: 1.sh, + child: Stack( + alignment: Alignment.center, + children: [ + Obx( + () { + final double screenWidth = MediaQuery.of(context).size.width; + final double screenHeight = MediaQuery.of(context).size.height; + + final double logicalWidth = MediaQuery.of(context).size.width; + final double logicalHeight = MediaQuery.of(context).size.height; + final double devicePixelRatio = + MediaQuery.of(context).devicePixelRatio; + + // 计算物理像素值 + final double physicalWidth = logicalWidth * devicePixelRatio; + final double physicalHeight = logicalHeight * devicePixelRatio; + + // 旋转后的图片尺寸 + const int rotatedImageWidth = 480; // 原始高度 + const int rotatedImageHeight = 864; // 原始宽度 + + // 计算缩放比例 + final double scaleWidth = physicalWidth / rotatedImageWidth; + final double scaleHeight = physicalHeight / rotatedImageHeight; + max(scaleWidth, scaleHeight); // 选择较大的缩放比例 + + return state.isLoading.isTrue + ? Image.asset( + 'images/main/monitorBg.png', + width: screenWidth, + height: screenHeight, + fit: BoxFit.cover, + ) + : PopScope( + canPop: false, + child: RepaintBoundary( + key: state.globalKey, + child: SizedBox.expand( + child: RotatedBox( + // 解码器不支持硬件旋转,使用RotatedBox + quarterTurns: -1, + child: Texture( + textureId: state.textureId.value!, + filterQuality: FilterQuality.medium, + ), + ), + ), + ), + ); + }, + ), + Obx(() => state.isLoading.isTrue + ? Positioned( + bottom: 310.h, + child: Text( + '正在创建安全连接...'.tr, + style: TextStyle(color: Colors.black, fontSize: 26.sp), + )) + : Container()), + Obx(() => state.textureId.value != null && state.showFps.value + ? Positioned( + top: ScreenUtil().statusBarHeight + 10.h, + right: 20.w, + child: Container( + padding: + EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.h), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + borderRadius: BorderRadius.circular(5.h), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + // Text( + // 'FPS: ${state.decoderFps.value.toStringAsFixed(1)}', + // style: TextStyle( + // color: _getPacketLossColor( + // state.packetLossRate.value), + // fontSize: 20.sp, + // ), + // ), + Text( + '丢包率: ${state.packetLossRate.value.toStringAsFixed(1)}%', + style: TextStyle( + color: _getPacketLossColor( + state.packetLossRate.value), + fontSize: 20.sp, + ), + ), + Text( + '消息丢失: ${state.messageLossRate.value.toStringAsFixed(1)}%', + style: TextStyle( + color: _getPacketLossColor( + state.messageLossRate.value), + fontSize: 20.sp, + ), + ), + Divider( + color: Colors.white30, + height: 10.h, + thickness: 1), + Text( + '已渲染帧: ${state.renderedFrameCount.value}', + style: + TextStyle(color: Colors.white, fontSize: 18.sp), + ), + Text( + '总帧数: ${state.totalFrames.value}', + style: + TextStyle(color: Colors.white, fontSize: 18.sp), + ), + Text( + '丢弃帧: ${state.droppedFrames.value}', + style: + TextStyle(color: Colors.white, fontSize: 18.sp), + ), + Text( + 'IDR帧: ${state.hasSentIDR.value ? "已发送" : "未发送"}', + style: TextStyle( + color: state.hasSentIDR.value + ? Colors.green + : Colors.red, + fontSize: 18.sp), + ), + Text( + 'SPS: ${state.hasSentSPS.value ? "已发送" : "未发送"}', + style: TextStyle( + color: state.hasSentSPS.value + ? Colors.green + : Colors.red, + fontSize: 18.sp), + ), + Text( + 'PPS: ${state.hasSentPPS.value ? "已发送" : "未发送"}', + style: TextStyle( + color: state.hasSentPPS.value + ? Colors.green + : Colors.red, + fontSize: 18.sp), + ), + Text( + 'keyFrameInterval: ${state.keyFrameInterval.value}', + style: + TextStyle(color: Colors.green, fontSize: 18.sp), + ), + Text( + 'decodingJitterMs: ${state.decodingJitterMs.value}', + style: + TextStyle(color: Colors.green, fontSize: 18.sp), + ), + ], + ), + ), + ) + : Container()), + Obx(() => state.isLoading.isFalse && state.oneMinuteTime.value > 0 + ? Positioned( + top: ScreenUtil().statusBarHeight + 75.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), + ), + ], + ); + }, + ), + ) + : 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), + ], + ), + ), + ), + Obx(() => state.isLoading.isTrue + ? buildRotationTransition() + : Container()), + Obx(() => state.isLongPressing.value + ? Positioned( + top: 80.h, + left: 0, + right: 0, + child: Center( + child: Container( + padding: EdgeInsets.all(10.w), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.7), + borderRadius: BorderRadius.circular(10.w), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.mic, color: Colors.white, size: 24.w), + SizedBox(width: 10.w), + Text( + '正在说话...'.tr, + style: TextStyle( + fontSize: 20.sp, color: Colors.white), + ), + ], + ), + ), + ), + ) + : Container()), + ], + ), + ), + ); + } + + Widget bottomTopBtnWidget() { + return Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + // 打开关闭声音 + GestureDetector( + onTap: () { + if (state.talkStatus.value == TalkStatus.answeredSuccessfully) { + // 打开关闭声音 + logic.updateTalkExpect(); + } + }, + 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_monitoringOpenVoice.png') + : const AssetImage( + 'images/main/icon_lockDetail_monitoringCloseVoice.png'))), + ), + ), + SizedBox(width: 50.w), + // 截图 + GestureDetector( + onTap: () async { + if (state.talkStatus.value == TalkStatus.answeredSuccessfully) { + await logic.captureAndSavePng(); + } + }, + 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: () async { + logic.showToast('功能暂未开放'.tr); + // if ( + // state.talkStatus.value == TalkStatus.answeredSuccessfully) { + // if (state.isRecordingScreen.value) { + // await logic.stopRecording(); + // } else { + // await logic.startRecording(); + // } + // } + }, + 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'), + ), + ), + ), + ]); + } + + Widget bottomBottomBtnWidget() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // 接听 + Obx( + () => bottomBtnItemWidget( + getAnswerBtnImg(), + getAnswerBtnName(), + Colors.white, + longPress: () async { + if (state.talkStatus.value == TalkStatus.answeredSuccessfully) { + // 启动录音 + logic.startProcessingAudio(); + state.isLongPressing.value = true; + } + }, + longPressUp: () async { + // 停止录音 + logic.stopProcessingAudio(); + state.isLongPressing.value = false; + }, + onClick: () async { + if (state.talkStatus.value == + TalkStatus.passiveCallWaitingAnswer) { + // 接听 + logic.initiateAnswerCommand(); + } + }, + ), + ), + bottomBtnItemWidget( + 'images/main/icon_lockDetail_hangUp.png', '挂断'.tr, Colors.red, + onClick: () { + // 挂断 + logic.udpHangUpAction(); + }), + bottomBtnItemWidget( + 'images/main/icon_lockDetail_monitoringUnlock.png', + '开锁'.tr, + AppColors.mainColor, + onClick: () { + // if (state.talkStatus.value == TalkStatus.answeredSuccessfully && + // state.listData.value.length > 0) { + // logic.udpOpenDoorAction(); + // } + // if (UDPManage().remoteUnlock == 1) { + // logic.udpOpenDoorAction(); + // showDeletPasswordAlertDialog(context); + // } else { + // logic.showToast('请在锁设置中开启远程开锁'.tr); + // } + logic.remoteOpenLock(); + }, + ) + ]); + } + + String getAnswerBtnImg() { + switch (state.talkStatus.value) { + case TalkStatus.passiveCallWaitingAnswer: + return 'images/main/icon_lockDetail_monitoringAnswerCalls.png'; + case TalkStatus.answeredSuccessfully: + case TalkStatus.proactivelyCallWaitingAnswer: + return 'images/main/icon_lockDetail_monitoringUnTalkback.png'; + default: + return 'images/main/icon_lockDetail_monitoringAnswerCalls.png'; + } + } + + String getAnswerBtnName() { + switch (state.talkStatus.value) { + case TalkStatus.passiveCallWaitingAnswer: + return '接听'.tr; + case TalkStatus.proactivelyCallWaitingAnswer: + case TalkStatus.answeredSuccessfully: + 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: 160.w, + width: 140.w, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: wh, + height: wh, + constraints: BoxConstraints( + minWidth: 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), + Text( + name, + style: TextStyle(fontSize: 20.sp, color: Colors.white), + textAlign: TextAlign.center, // 当文本超出指定行数时,使用省略号表示 + maxLines: 2, // 设置最大行数为1 + ) + ], + ), + ), + ); + } + + // 根据丢包率返回对应的颜色 + Color _getPacketLossColor(double lossRate) { + if (lossRate < 1.0) { + return Colors.green; // 丢包率低于1%显示绿色 + } else if (lossRate < 5.0) { + return Colors.yellow; // 丢包率1%-5%显示黄色 + } else if (lossRate < 10.0) { + return Colors.orange; // 丢包率5%-10%显示橙色 + } else { + return Colors.red; // 丢包率高于10%显示红色 + } + } + + //旋转动画 + 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(); + CallTalk().finishAVData(); + super.dispose(); + } +} diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_state.dart b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart new file mode 100644 index 00000000..d98d5d34 --- /dev/null +++ b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart @@ -0,0 +1,109 @@ +import 'dart:async'; +import 'dart:typed_data'; +import 'dart:ui' as ui; +import 'dart:io'; + +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:get/get_rx/src/rx_types/rx_types.dart'; +import 'package:get/state_manager.dart'; +import 'package:network_info_plus/network_info_plus.dart'; +import 'package:star_lock/talk/starChart/constant/talk_status.dart'; +import 'package:star_lock/talk/starChart/handle/other/packet_loss_statistics.dart'; +import 'package:star_lock/talk/starChart/handle/other/talk_data_repository.dart'; +import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart'; +import 'package:star_lock/talk/starChart/status/star_chart_talk_status.dart'; +import 'package:video_decode_plugin/video_decode_plugin.dart'; + +import '../../../../tools/storage.dart'; + +enum NetworkStatus { + normal, // 0 + lagging, // 1 + delayed, // 2 + packetLoss // 3 +} + +class TalkViewNativeDecodeState { + // 视频源最大帧率限制 + static const int maxSourceFps = 25; // 视频源最高支持25fps + + 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(); + + RxList listAudioData = [].obs; //得到的音频流字节数据 + GlobalKey globalKey = GlobalKey(); + + Timer? oneMinuteTimeTimer; // 定时器超过60秒关闭当前界面 + RxInt oneMinuteTime = 0.obs; // 定时器秒数 + + // 定时器如果发送了接听的命令 而没收到回复就每秒重复发送10次 + late Timer answerTimer; + late Timer hangUpTimer; + late Timer openDoorTimer; + Timer? fpsTimer; + late AnimationController animationController; + + RxInt elapsedSeconds = 0.obs; + + // 星图对讲相关状态 + List audioBuffer = [].obs; + + RxBool isLoading = true.obs; // 是否在加载 + RxBool isPlaying = false.obs; // 是否开始播放 + Rx talkStatus = TalkStatus.none.obs; //星图对讲状态 + // 获取 startChartTalkStatus 的唯一实例 + final StartChartTalkStatus startChartTalkStatus = + StartChartTalkStatus.instance; + + // 通话数据流的单例流数据处理类 + final TalkDataRepository talkDataRepository = TalkDataRepository.instance; + + RxBool isOpenVoice = true.obs; // 是否打开声音 + RxBool isRecordingScreen = false.obs; // 是否录屏中 + RxBool isRecordingAudio = false.obs; // 是否录音中 + Rx startRecordingAudioTime = DateTime.now().obs; // 开始录音时间 + Rx endRecordingAudioTime = DateTime.now().obs; // 结束录音时间 + RxInt recordingAudioTime = 0.obs; // 录音时间持续时间 + late VoiceProcessor? voiceProcessor; // 音频处理器、录音 + final int frameLength = 320; //录音视频帧长度为640 + final int sampleRate = 8000; //录音频采样率为8000 + RxBool isLongPressing = false.obs; // 旋转角度(以弧度为单位) + // 视频解码器纹理ID + Rx textureId = Rx(null); + // FPS监测相关变量 + + RxInt lastFpsUpdateTime = 0.obs; // 上次FPS更新时间 + RxBool showFps = true.obs; // 是否显示FPS + // 丢包率统计相关变量 + RxDouble decoderFps = 0.0.obs; // 消息丢失率 + RxDouble messageLossRate = 0.0.obs; // 消息丢失率 + RxDouble packetLossRate = 0.0.obs; // 分包丢失率 + RxInt lastPacketStatsUpdateTime = 0.obs; // 上次更新丢包统计的时间 + + // 解码器详细统计信息 + RxInt renderedFrameCount = 0.obs; // 已渲染帧数 + RxInt totalFrames = 0.obs; // 总帧数 + RxInt droppedFrames = 0.obs; // 丢弃帧数 + RxBool hasSentIDR = false.obs; // 是否已发送IDR帧 + RxBool hasSentSPS = false.obs; // 是否已发送SPS + RxBool hasSentPPS = false.obs; // 是否已发送PPS + RxInt keyFrameInterval = 0.obs; // 关键帧间隔时间(ms) + RxInt decodingJitterMs = 0.obs; // 解码抖动时间(ms) + + // 性能分析变量 + int lastPerformanceCheck = 0; + int lastFrameCount = 0; + + // 帧跟踪Map,记录每个提交的帧,key为textureId_frameSeq + Map> frameTracker = {}; +} diff --git a/lib/talk/starChart/views/talkView/talk_view_logic.dart b/lib/talk/starChart/views/talkView/talk_view_logic.dart index a4a850ba..eaf051c1 100644 --- a/lib/talk/starChart/views/talkView/talk_view_logic.dart +++ b/lib/talk/starChart/views/talkView/talk_view_logic.dart @@ -45,12 +45,12 @@ class TalkViewLogic extends BaseGetXController { final int minAudioBufferSize = 1; // 音频最小缓冲1帧 final int maxAudioBufferSize = 3; // 音频最大缓冲3帧 int audioBufferSize = 2; // 音频默认缓冲2帧 - + bool _isFirstAudioFrame = true; // 是否是第一帧 // 添加开始时间记录 int _startTime = 0; // 开始播放时间戳 int _startAudioTime = 0; // 开始播放时间戳 bool _isFirstFrame = true; // 是否是第一帧 - bool _isFirstAudioFrame = true; // 是否是第一帧 + // 定义音频帧缓冲和发送函数 final List _bufferedAudioFrames = []; @@ -106,6 +106,24 @@ class TalkViewLogic extends BaseGetXController { // 判断数据类型,进行分发处理 switch (contentType) { case TalkData_ContentTypeE.G711: + // // 第一帧到达时记录开始时间 + if (_isFirstAudioFrame) { + _startAudioTime = currentTime; + _isFirstAudioFrame = false; + } + + // 计算音频延迟 + final expectedTime = _startAudioTime + talkData.durationMs; + final audioDelay = currentTime - expectedTime; + + // 如果延迟太大,清空缓冲区并直接播放 + if (audioDelay > 500) { + state.audioBuffer.clear(); + if (state.isOpenVoice.value) { + _playAudioFrames(); + } + return; + } if (state.audioBuffer.length >= audioBufferSize) { state.audioBuffer.removeAt(0); // 丢弃最旧的数据 } @@ -118,7 +136,7 @@ class TalkViewLogic extends BaseGetXController { if (_isFirstFrame) { _startTime = currentTime; _isFirstFrame = false; - AppLog.log('第一帧帧的时间戳:${talkData.durationMs}'); + // AppLog.log('第一帧帧的时间戳:${talkData.durationMs}'); } // AppLog.log('其他帧的时间戳:${talkData.durationMs}'); // 计算帧间间隔 @@ -366,19 +384,6 @@ class TalkViewLogic extends BaseGetXController { } } - // 获取手机联网token,根据锁设置里面获取的开锁时是否联网来判断是否调用这个接口 - Future _getLockNetToken() async { - final LockNetTokenEntity entity = await ApiRepository.to.getLockNetToken( - lockId: lockDetailState.keyInfos.value.lockId.toString()); - if (entity.errorCode!.codeIsSuccessful) { - lockDetailState.lockNetToken = entity.data!.token!.toString(); - AppLog.log('从服务器获取联网token:${lockDetailState.lockNetToken}'); - } else { - BuglyTool.uploadException( - message: '点击了需要联网开锁', detail: '点击了需要联网开锁 获取连网token失败', upload: true); - showToast('网络访问失败,请检查网络是否正常'.tr, something: () {}); - } - } /// 获取权限状态 Future getPermissionStatus() async { diff --git a/lib/talk/starChart/webView/h264_web_logic.dart b/lib/talk/starChart/webView/h264_web_logic.dart index 626dff04..4f5789c2 100644 --- a/lib/talk/starChart/webView/h264_web_logic.dart +++ b/lib/talk/starChart/webView/h264_web_logic.dart @@ -48,7 +48,7 @@ class H264WebViewLogic extends BaseGetXController { Timer? _mockDataTimer; int _startAudioTime = 0; // 开始播放时间戳 int audioBufferSize = 2; // 音频默认缓冲2帧 - + bool _isFirstAudioFrame = true; // 是否是第一帧 // 定义音频帧缓冲和发送函数 final List _bufferedAudioFrames = []; final Queue> _frameBuffer = Queue>(); @@ -131,6 +131,24 @@ class H264WebViewLogic extends BaseGetXController { // 判断数据类型,进行分发处理 switch (contentType) { case TalkData_ContentTypeE.G711: + // // 第一帧到达时记录开始时间 + if (_isFirstAudioFrame) { + _startAudioTime = currentTime; + _isFirstAudioFrame = false; + } + + // 计算音频延迟 + final expectedTime = _startAudioTime + talkData.durationMs; + final audioDelay = currentTime - expectedTime; + + // 如果延迟太大,清空缓冲区并直接播放 + if (audioDelay > 500) { + state.audioBuffer.clear(); + if (state.isOpenVoice.value) { + _playAudioFrames(); + } + return; + } if (state.audioBuffer.length >= audioBufferSize) { state.audioBuffer.removeAt(0); // 丢弃最旧的数据 } diff --git a/pubspec.yaml b/pubspec.yaml index 454bd944..331117fb 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -127,7 +127,8 @@ dependencies: sdk: flutter aliyun_face_plugin: path: aliyun_face_plugin - + video_decode_plugin: + path: ../video_decode_plugin flutter_localizations: sdk: flutter From 2a89d0110c01a6808272bfa31e422f6b958063d3 Mon Sep 17 00:00:00 2001 From: liyi Date: Sat, 26 Apr 2025 15:15:27 +0800 Subject: [PATCH 034/151] =?UTF-8?q?fix:=E6=A2=B3=E7=90=86=E9=85=8D?= =?UTF-8?q?=E7=BD=91=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuringWifi_logic.dart | 528 ++++++++++++------ .../configuringWifi/configuringWifi_page.dart | 104 +++- .../configuringWifi_state.dart | 3 - .../wifiList/wifiList_logic.dart | 101 +++- .../wifiList/wifiList_page.dart | 145 +++-- 5 files changed, 626 insertions(+), 255 deletions(-) diff --git a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart index 25c8e43d..a940ca4e 100755 --- a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart +++ b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart @@ -37,15 +37,24 @@ import 'configuringWifi_state.dart'; class ConfiguringWifiLogic extends BaseGetXController { final ConfiguringWifiState state = ConfiguringWifiState(); + final int _configurationTimeout = 60; // 配网超时时间(秒) + /// 获取WiFi锁服务IP和端口 Future getWifiLockServiceIpAndPort() async { - final ConfiguringWifiEntity entity = - await ApiRepository.to.getWifiLockServiceIpAndPort(); - if (entity.errorCode! == 0) { - state.configuringWifiEntity.value = entity; + try { + final ConfiguringWifiEntity entity = + await ApiRepository.to.getWifiLockServiceIpAndPort(); + if (entity.errorCode! == 0) { + state.configuringWifiEntity.value = entity; + } else { + AppLog.log('获取WiFi锁服务IP和端口失败:${entity.errorCode}'); + } + } catch (e) { + AppLog.log('获取WiFi锁服务IP和端口异常:$e'); } } + /// 更新网络信息到服务器 void updateNetworkInfo({ required String peerId, required String wifiName, @@ -53,24 +62,36 @@ class ConfiguringWifiLogic extends BaseGetXController { required String deviceMac, required String networkMac, }) async { - final LoginEntity entity = await ApiRepository.to.settingDeviceNetwork( - deviceType: 2, - deviceMac: deviceMac, - wifiName: wifiName, - networkMac: networkMac, - secretKey: secretKey, - peerId: peerId, - ); - if (entity.errorCode!.codeIsSuccessful) { - // 设置锁的peerID - StartChartManage().lockNetworkInfo = DeviceNetworkInfo( + try { + final LoginEntity entity = await ApiRepository.to.settingDeviceNetwork( + deviceType: 2, + deviceMac: deviceMac, wifiName: wifiName, networkMac: networkMac, secretKey: secretKey, peerId: peerId, ); - await _getUploadLockSet(); + if (entity.errorCode!.codeIsSuccessful) { + // 设置锁的peerID + StartChartManage().lockNetworkInfo = DeviceNetworkInfo( + wifiName: wifiName, + networkMac: networkMac, + secretKey: secretKey, + peerId: peerId, + ); + + await _getUploadLockSet(); + } else { + dismissEasyLoading(); + showToast('网络配置失败,请重试'.tr); + state.sureBtnState.value = 0; + } + } catch (e) { + dismissEasyLoading(); + showToast('网络配置异常:${e.toString()}'.tr); + state.sureBtnState.value = 0; + AppLog.log('网络配置异常:$e'); } } @@ -84,81 +105,138 @@ class ConfiguringWifiLogic extends BaseGetXController { if (reply is GatewayConfiguringWifiResultReply) { _replySenderConfiguringWifiResult(reply); } + // wifi配网命令应答结果 if (reply is GatewayConfiguringWifiReply) { - _replySenderConfiguringWifiResult(reply); + _replySenderConfiguringWifi(reply); } if (reply is GatewayGetStatusReply) { _replyGatewayGetStatusReply(reply); } - // if (reply is GatewayGetStatusReply) { - // _replyStatusInfo(reply); - // } // 上传数据获取锁设置 if (reply is UpdataLockSetReply) { _replyUpdataLockSetReply(reply); } - AppLog.log('蓝牙回调处理完毕${EasyLoading.isShow}'); }); } - // WIFI配网结果 - Future _replySenderConfiguringWifiResult(Reply reply) async { + // WIFI配网操作结果处理 + Future _replySenderConfiguringWifi(Reply reply) async { final int status = reply.data[2]; - // state.sureBtnState.value = 0; - - // 取消loading超时定时器 - state.loadingTimer?.cancel(); - state.loadingTimer = null; switch (status) { case 0x00: - await Storage.removeLockNetWorkInfoCache(); - final int secretKeyJsonLength = (reply.data[4] << 8) + reply.data[3]; - - final List secretKeyList = - reply.data.sublist(5, 5 + secretKeyJsonLength); - String result = utf8String(secretKeyList); - // 解析 JSON 字符串为 Map - Map jsonMap = json.decode(result); - - // 提取 peerId - String? peerId = jsonMap['peerId']; - String? wifiName = jsonMap['wifiName']; - String? secretKey = jsonMap['secretKey']; - String? deviceMac = jsonMap['deviceMac']; - String? networkMac = jsonMap['networkMac']; - - /// 配网成功后,赋值锁的peerId - StartChartManage().lockPeerId = peerId ?? ''; - - state.isLoading.value = false; - // 保存到缓存 - await Storage.saveLockNetWorkInfo(jsonMap); - // 上报服务器 - updateNetworkInfo( - peerId: peerId ?? '', - wifiName: wifiName ?? '', - secretKey: secretKey ?? '', - deviceMac: deviceMac ?? '', - networkMac: networkMac ?? ''); - + AppLog.log('wifi配网命令回复结果:成功'); break; default: //失败 dismissEasyLoading(); // 关闭loading - cancelBlueConnetctToastTimer(); - if (state.loadingTimer != null) { - state.loadingTimer!.cancel(); - state.loadingTimer = null; - } - showToast('配网失败'.tr); state.isLoading.value = false; break; } } -// 辅助函数:美化 JSON 输出 + // WIFI配网结果处理 + Future _replySenderConfiguringWifiResult(Reply reply) async { + final int status = reply.data[2]; + + // 收到响应后,取消蓝牙超时计时器 + cancelBlueConnetctToastTimer(); + + switch (status) { + case 0x00: + // 配网成功 - 不关闭loading,保持状态直到全部完成 + await Storage.removeLockNetWorkInfoCache(); + + try { + final int secretKeyJsonLength = (reply.data[4] << 8) + reply.data[3]; + final List secretKeyList = + reply.data.sublist(5, 5 + secretKeyJsonLength); + String result = utf8String(secretKeyList); + + AppLog.log('解析配网信息: $result'); + + // 解析 JSON 字符串为 Map + Map jsonMap = json.decode(result); + + // 提取网络信息 + String? peerId = jsonMap['peerId']; + String? wifiName = jsonMap['wifiName']; + String? secretKey = jsonMap['secretKey']; + String? deviceMac = jsonMap['deviceMac']; + String? networkMac = jsonMap['networkMac']; + + // 验证关键字段 + if (peerId == null || + peerId.isEmpty || + secretKey == null || + secretKey.isEmpty) { + throw Exception('Missing required network information'); + } + + /// 配网成功后,赋值锁的peerId + StartChartManage().lockPeerId = peerId; + + // 保存到缓存 + await Storage.saveLockNetWorkInfo(jsonMap); + + // 上报服务器 - 注意: sureBtnState 状态将在 updateNetworkInfo 方法中或其回调中完成重置 + updateNetworkInfo( + peerId: peerId, + wifiName: wifiName ?? '', + secretKey: secretKey, + deviceMac: deviceMac ?? '', + networkMac: networkMac ?? ''); + } catch (e) { + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + showToast('解析配网信息失败,请重试'.tr); + state.sureBtnState.value = 0; // 确保重置状态 + AppLog.log('解析配网信息失败: $e'); + return; // 添加return阻止后续流程 + } + break; + + case 0x01: + // WiFi密码错误 + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + showToast('WiFi密码错误,请重新输入'.tr); + state.sureBtnState.value = 0; // 确保重置状态 + break; + + case 0x02: + // 找不到WiFi + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + showToast('找不到该WiFi网络,请确认WiFi名称正确'.tr); + state.sureBtnState.value = 0; // 确保重置状态 + break; + + case 0x03: + // 网络连接超时 + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + showToast('连接WiFi超时,请确保网络信号良好'.tr); + state.sureBtnState.value = 0; // 确保重置状态 + break; + + default: + // 其他错误 + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + showToast('配网失败 (错误码: $status),请重试'.tr); + state.sureBtnState.value = 0; // 确保重置状态 + break; + } + } + + // 辅助函数:美化 JSON 输出 String prettyPrintJson(String jsonString) { var jsonObject = json.decode(jsonString); return JsonEncoder.withIndent(' ').convert(jsonObject); @@ -168,63 +246,80 @@ class ConfiguringWifiLogic extends BaseGetXController { Future senderConfiguringWifiAction() async { AppLog.log('开始配网${EasyLoading.isShow}'); - if (state.isLoading.isTrue) { + if (state.sureBtnState.value == 1) { AppLog.log('正在配网中请勿重复点击'); return; } - if (state.wifiNameController.text.isEmpty) { - showToast('请输入wifi名称'.tr); + + // 获取网关配置信息 + try { + final GetGatewayConfigurationEntity entity = await ApiRepository.to + .getGatewayConfigurationNotLoading(timeout: _configurationTimeout); + if (entity.errorCode!.codeIsSuccessful) { + state.getGatewayConfigurationStr = entity.data ?? ''; + } else { + // showToast('获取网关配置失败,请重试'.tr); + AppLog.log('获取网关配置失败,请重试'); + return; + } + + // 判断是否登录账户 + final loginData = await Storage.getLoginData(); + if (loginData == null) { + AppLog.log('未检测到登录信息,请重新登录'.tr); + return; + } + + // 获取app用户的peerId + String appPeerId = loginData.starchart?.starchartId ?? ''; + if (appPeerId.isEmpty) { + AppLog.log('用户ID获取失败,请重新登录'.tr); + return; + } + + // 处理配置字符串 + if (state.getGatewayConfigurationStr.isNotEmpty) { + // 解析 JSON 字符串为 Map + Map jsonMap = + json.decode(state.getGatewayConfigurationStr); + + // 移除指定的键 + jsonMap.remove("starCloudUrl"); + jsonMap.remove("starLockPeerId"); + + // 追加新的键值对 + jsonMap['userPeerld'] = appPeerId; + + // 将 Map 转换回 JSON 字符串 + state.getGatewayConfigurationStr = + json.encode(jsonMap).replaceAll(',', ',\n'); + + // 确保格式化输出 + state.getGatewayConfigurationStr = + prettyPrintJson(state.getGatewayConfigurationStr); + } else { + // 如果为空,则直接赋值 + state.getGatewayConfigurationStr = "{\"userPeerld\": \"$appPeerId\"}"; + } + } catch (e) { + AppLog.log('网关配置准备失败:${e.toString()}'.tr); return; } - if (state.wifiPWDController.text.isEmpty) { - showToast('请输入WiFi密码'.tr); - return; - } - // if (state.sureBtnState.value == 1) { - // return; - // } - // state.sureBtnState.value = 1; + // 先设置sureBtnState状态,以禁用按钮 + state.sureBtnState.value = 1; - final GetGatewayConfigurationEntity entity = - await ApiRepository.to.getGatewayConfigurationNotLoading(timeout: 60); - if (entity.errorCode!.codeIsSuccessful) { - state.getGatewayConfigurationStr = entity.data ?? ''; + // 显示loading,如果已经显示则不再重复显示 + if (!EasyLoading.isShow) { + showEasyLoading(); } - // 判断是否登录账户 - final loginData = await Storage.getLoginData(); - - // 获取app用户的peerId - String appPeerId = loginData?.starchart?.starchartId ?? ''; - // 如果已有值,则追加 - if (state.getGatewayConfigurationStr.isNotEmpty) { - // 解析 JSON 字符串为 Map - Map jsonMap = - json.decode(state.getGatewayConfigurationStr); - - // 移除指定的键 - jsonMap.remove("starCloudUrl"); - jsonMap.remove("starLockPeerId"); - - // 追加新的键值对 - jsonMap['userPeerld'] = appPeerId; - - // 将 Map 转换回 JSON 字符串 - state.getGatewayConfigurationStr = - json.encode(jsonMap).replaceAll(',', ',\n'); - - // 确保格式化输出 - state.getGatewayConfigurationStr = - prettyPrintJson(state.getGatewayConfigurationStr); - } else { - // 如果为空,则直接赋值 - state.getGatewayConfigurationStr = "{\"userPeerld\": \"$appPeerId\"}"; - } - showEasyLoading(); + // 设置蓝牙操作超时处理 showBlueConnetctToastTimer(action: () { - dismissEasyLoading(); - state.isLoading.value = false; + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + state.sureBtnState.value = 0; // 连接超时时重置状态 }); // 发送配网指令 @@ -232,16 +327,27 @@ class ConfiguringWifiLogic extends BaseGetXController { BlueManage().connectDeviceName, (BluetoothConnectionState connectionState) async { if (connectionState == BluetoothConnectionState.connected) { - IoSenderManage.gatewayConfiguringWifiCommand( - ssid: state.wifiNameController.text, - password: state.wifiPWDController.text, - gatewayConfigurationStr: state.getGatewayConfigurationStr, - ); + try { + IoSenderManage.gatewayConfiguringWifiCommand( + ssid: state.wifiNameController.text, + password: state.wifiPWDController.text, + gatewayConfigurationStr: state.getGatewayConfigurationStr, + ); + // 注意:此处不要重置sureBtnState状态,等待配网结果回调 + } catch (e) { + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + cancelBlueConnetctToastTimer(); + state.sureBtnState.value = 0; // 发送命令失败时重置状态 + showToast('发送配网指令失败:${e.toString()}'.tr); + } } else if (connectionState == BluetoothConnectionState.disconnected) { - dismissEasyLoading(); + if (EasyLoading.isShow) { + dismissEasyLoading(); + } cancelBlueConnetctToastTimer(); - state.isLoading.value = false; - // state.sureBtnState.value = 0; + state.sureBtnState.value = 0; // 蓝牙断开时重置状态 if (state.ifCurrentScreen.value == true) { showBlueConnetctToast(); } @@ -249,7 +355,6 @@ class ConfiguringWifiLogic extends BaseGetXController { }, isAddEquipment: false, ); - state.isLoading.value = true; } // 获取设备状态 @@ -278,11 +383,15 @@ class ConfiguringWifiLogic extends BaseGetXController { final NetworkInfo _networkInfo = NetworkInfo(); Future getWifiName() async { - String ssid = ''; - ssid = (await _networkInfo.getWifiName())!; - ssid = ssid ?? ''; - ssid = ssid.replaceAll(r'"', ''); - return ssid ?? ''; + try { + String? ssid = await _networkInfo.getWifiName(); + ssid = ssid ?? ''; + ssid = ssid.replaceAll(r'"', ''); + return ssid; + } catch (e) { + AppLog.log('获取WiFi名称失败: $e'); + return ''; + } } ///定位权限 @@ -308,17 +417,12 @@ class ConfiguringWifiLogic extends BaseGetXController { getWifiLockServiceIpAndPort(); _initReplySubscription(); - // getDevicesStatusAction(); - } - - @override - void onInit() { - super.onInit(); } @override void onClose() { _replySubscription.cancel(); + cancelBlueConnetctToastTimer(); // 确保取消蓝牙超时计时器 super.onClose(); } @@ -330,8 +434,6 @@ class ConfiguringWifiLogic extends BaseGetXController { switch (status) { case 0x00: //成功 - // state.sureBtnState.value = 0; - final GetGatewayInfoModel gatewayModel = GetGatewayInfoModel(); // 网关MAC地址 int index = 3; @@ -372,70 +474,108 @@ class ConfiguringWifiLogic extends BaseGetXController { default: //失败 dismissEasyLoading(); - showToast('配网失败'.tr); - if (state.loadingTimer != null) { - state.loadingTimer!.cancel(); - state.loadingTimer = null; - } + showToast('获取设备状态失败'.tr); break; } } // 上传数据获取设置 Future _getUploadLockSet() async { - showEasyLoading(); + // 保持已有的loading状态,不再重新显示loading showBlueConnetctToastTimer(action: () { - dismissEasyLoading(); + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + state.sureBtnState.value = 0; }); - final List? token = await Storage.getStringList(saveBlueToken); - final List getTokenList = changeStringListToIntList(token!); - - await _uploadLockSet(getTokenList); + try { + final List? token = await Storage.getStringList(saveBlueToken); + if (token == null || token.isEmpty) { + throw Exception('Token is empty'); + } + final List getTokenList = changeStringListToIntList(token); + await _uploadLockSet(getTokenList); + } catch (e) { + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + cancelBlueConnetctToastTimer(); + showToast('获取设置失败:${e.toString()}'.tr); + state.sureBtnState.value = 0; + } } // 公共的上传锁设置 Future _uploadLockSet(List token) async { - final List? privateKey = - await Storage.getStringList(saveBluePrivateKey); - final List getPrivateKeyList = changeStringListToIntList(privateKey!); + try { + final List? privateKey = + await Storage.getStringList(saveBluePrivateKey); + if (privateKey == null || privateKey.isEmpty) { + throw Exception('Private key is empty'); + } + final List getPrivateKeyList = changeStringListToIntList(privateKey); - final List? signKey = await Storage.getStringList(saveBlueSignKey); - final List signKeyDataList = changeStringListToIntList(signKey!); + final List? signKey = + await Storage.getStringList(saveBlueSignKey); + if (signKey == null || signKey.isEmpty) { + throw Exception('Sign key is empty'); + } + final List signKeyDataList = changeStringListToIntList(signKey); - IoSenderManage.updataLockSetCommand( - lockID: BlueManage().connectDeviceName, - userID: await Storage.getUid(), - token: token, - needAuthor: 1, - signKey: signKeyDataList, - privateKey: getPrivateKeyList); + IoSenderManage.updataLockSetCommand( + lockID: BlueManage().connectDeviceName, + userID: await Storage.getUid(), + token: token, + needAuthor: 1, + signKey: signKeyDataList, + privateKey: getPrivateKeyList); + } catch (e) { + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + cancelBlueConnetctToastTimer(); + showToast('上传设置失败:${e.toString()}'.tr); + state.sureBtnState.value = 0; + } } // 上传数据获取锁设置解析 Future _replyUpdataLockSetReply(Reply reply) async { final int status = reply.data[2]; - dismissEasyLoading(); // 关闭loading + // 保持loading状态直到整个过程完成 cancelBlueConnetctToastTimer(); + switch (status) { case 0x00: await _lockDataUpload( uploadType: 1, recordType: 0, records: reply.data.sublist(7, reply.data.length)); - break; + case 0x06: - //无权限 - final List token = reply.data.sublist(3, 7); - final List saveStrList = changeIntListToStringList(token); - Storage.setStringList(saveBlueToken, saveStrList); - - _uploadLockSet(token); + //无权限,尝试重新获取token + try { + final List token = reply.data.sublist(3, 7); + final List saveStrList = changeIntListToStringList(token); + await Storage.setStringList(saveBlueToken, saveStrList); + _uploadLockSet(token); + } catch (e) { + if (EasyLoading.isShow) { + dismissEasyLoading(); // 错误时关闭loading + } + showToast('获取设置权限失败:${e.toString()}'.tr); + state.sureBtnState.value = 0; // 确保重置状态 + } break; + default: - dismissEasyLoading(); - cancelBlueConnetctToastTimer(); + if (EasyLoading.isShow) { + dismissEasyLoading(); // 错误时关闭loading + } + showToast('获取锁设置失败 (错误码: $status)'.tr); + state.sureBtnState.value = 0; // 确保重置状态 break; } } @@ -445,25 +585,43 @@ class ConfiguringWifiLogic extends BaseGetXController { {required int uploadType, required int recordType, required List records}) async { - final LoginEntity entity = await ApiRepository.to.lockDataUpload( - lockId: state.lockBasicInfo.value.lockId ?? -1, - uploadType: uploadType, - recordType: recordType, - records: records, - isUnShowLoading: true); - if (entity.errorCode!.codeIsSuccessful) { - showToast('配网成功'.tr, something: () { - state.isLoading.value = false; - if (state.pageName.value == 'lockSet') { - Get.close(2); - } else { - Get.offAllNamed(Routers.starLockMain); - } + try { + final LoginEntity entity = await ApiRepository.to.lockDataUpload( + lockId: state.lockBasicInfo.value.lockId ?? -1, + uploadType: uploadType, + recordType: recordType, + records: records, + isUnShowLoading: true); - eventBus - .fire(PassCurrentLockInformationEvent(state.lockSetInfoData.value)); - eventBus.fire(SuccessfulDistributionNetwork()); - }); + if (entity.errorCode!.codeIsSuccessful) { + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + showToast('配网成功'.tr, something: () { + state.sureBtnState.value = 0; // 确保重置状态 + if (state.pageName.value == 'lockSet') { + Get.close(2); + } else { + Get.offAllNamed(Routers.starLockMain); + } + + eventBus.fire( + PassCurrentLockInformationEvent(state.lockSetInfoData.value)); + eventBus.fire(SuccessfulDistributionNetwork()); + }); + } else { + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + showToast('数据上传失败:${entity.errorCode}'.tr); + state.sureBtnState.value = 0; // 确保重置状态 + } + } catch (e) { + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + showToast('数据上传异常:${e.toString()}'.tr); + state.sureBtnState.value = 0; // 确保重置状态 } } } diff --git a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_page.dart b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_page.dart index ed653308..51c45a9d 100755 --- a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_page.dart +++ b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_page.dart @@ -25,6 +25,9 @@ class _ConfiguringWifiPageState extends State final ConfiguringWifiLogic logic = Get.put(ConfiguringWifiLogic()); final ConfiguringWifiState state = Get.find().state; + // 添加密码可见性控制 + final RxBool _obscureText = true.obs; + @override Widget build(BuildContext context) { return Scaffold( @@ -39,19 +42,36 @@ class _ConfiguringWifiPageState extends State 'WiFi名称'.tr, '请输入WiFi名字'.tr, state.wifiNameController), Container( width: 1.sw, height: 1.h, color: AppColors.mainBackgroundColor), - configuringWifiTFWidget( + configuringWifiPasswordTFWidget( 'WiFi密码'.tr, '请输入WiFi密码'.tr, state.wifiPWDController), SizedBox( height: 50.h, ), Obx( () => SubmitBtn( - btnName: '确定'.tr, - isDisabled: state.isLoading.isFalse, - onClick: state.isLoading.isTrue + btnName: state.sureBtnState.value == 1 ? '配置中...'.tr : '确定'.tr, + // 当sureBtnState为1时按钮不可用 + isDisabled: state.sureBtnState.value == 1, + onClick: state.sureBtnState.value == 1 ? null : () { FocusScope.of(context).requestFocus(FocusNode()); + // 验证输入 + if (state.wifiNameController.text.isEmpty) { + logic.showToast('请输入WiFi名称'.tr); + return; + } + if (state.wifiPWDController.text.isEmpty) { + logic.showToast('请输入WiFi密码'.tr); + return; + } + // 检查WiFi名称是否包含5G关键字 + if (state.wifiNameController.text + .toLowerCase() + .contains('5g')) { + logic.showToast('请确保使用2.4GHz WiFi网络'.tr); + return; + } logic.senderConfiguringWifiAction(); }, ), @@ -86,7 +106,22 @@ class _ConfiguringWifiPageState extends State ); } - // 接受者信息输入框 + Widget configuringWifiPasswordTFWidget( + String titleStr, String rightTitle, TextEditingController controller) { + return Column( + children: [ + Container(height: 10.h), + CommonItem( + leftTitel: titleStr, + rightTitle: '', + isHaveRightWidget: true, + rightWidget: getPasswordTFWidget(rightTitle, controller)), + Container(height: 10.h), + ], + ); + } + + // 普通输入框 Widget getTFWidget(String tfStr, TextEditingController controller) { return Container( height: 65.h, @@ -95,18 +130,14 @@ class _ConfiguringWifiPageState extends State children: [ Expanded( child: TextField( - //输入框一行 maxLines: 1, inputFormatters: [ FilteringTextInputFormatter.deny('\n'), - // LengthLimitingTextInputFormatter(30), ], controller: controller, autofocus: false, textAlign: TextAlign.end, decoration: InputDecoration( - //输入里面输入文字内边距设置 - // contentPadding: const EdgeInsets.only(top: 12.0, bottom: 8.0), hintText: tfStr, hintStyle: TextStyle(fontSize: 22.sp), focusedBorder: const OutlineInputBorder( @@ -135,6 +166,61 @@ class _ConfiguringWifiPageState extends State ); } + // 密码输入框 + Widget getPasswordTFWidget(String tfStr, TextEditingController controller) { + return Container( + height: 65.h, + width: 300.w, + child: Row( + children: [ + Expanded( + child: Obx( + () => TextField( + maxLines: 1, + obscureText: _obscureText.value, + inputFormatters: [ + FilteringTextInputFormatter.deny('\n'), + ], + controller: controller, + autofocus: false, + textAlign: TextAlign.end, + decoration: InputDecoration( + hintText: tfStr, + hintStyle: TextStyle(fontSize: 22.sp), + focusedBorder: const OutlineInputBorder( + borderSide: + BorderSide(width: 0, color: Colors.transparent)), + disabledBorder: const OutlineInputBorder( + borderSide: + BorderSide(width: 0, color: Colors.transparent)), + enabledBorder: const OutlineInputBorder( + borderSide: + BorderSide(width: 0, color: Colors.transparent)), + border: const OutlineInputBorder( + borderSide: + BorderSide(width: 0, color: Colors.transparent)), + contentPadding: const EdgeInsets.symmetric(vertical: 0), + ), + style: TextStyle( + fontSize: 22.sp, textBaseline: TextBaseline.alphabetic), + ), + ), + ), + IconButton( + icon: Icon( + _obscureText.value ? Icons.visibility_off : Icons.visibility, + color: Colors.grey, + size: 24.sp, + ), + onPressed: () { + _obscureText.value = !_obscureText.value; + }, + ), + ], + ), + ); + } + @override void didChangeDependencies() { super.didChangeDependencies(); diff --git a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_state.dart b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_state.dart index 513fede2..350a2863 100755 --- a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_state.dart +++ b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_state.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:network_info_plus/network_info_plus.dart'; @@ -33,5 +31,4 @@ class ConfiguringWifiState { String getGatewayConfigurationStr = ''; RxBool isLoading = false.obs; - Timer? loadingTimer; } diff --git a/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_logic.dart b/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_logic.dart index 8f95204e..6ee5bea6 100755 --- a/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_logic.dart +++ b/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_logic.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:get/get.dart'; +import 'package:star_lock/app_settings/app_settings.dart'; import 'package:star_lock/blue/io_gateway/io_gateway_getWifiList.dart'; import 'package:star_lock/blue/io_protocol/io_getWifiList.dart'; import 'package:star_lock/talk/starChart/star_chart_manage.dart'; @@ -20,7 +21,9 @@ class WifiListLogic extends BaseGetXController { // 获取解析后的数据 late StreamSubscription _replySubscription; + Timer? _connectionTimer; + /// 初始化订阅,监听设备响应 void _initReplySubscription() { _replySubscription = EventBusManager().eventBus!.on().listen((Reply reply) { @@ -31,78 +34,130 @@ class WifiListLogic extends BaseGetXController { if (reply is GatewayGetWifiListReply) { _replyGetWifiListParameters(reply); } + }, onError: (error) { + // 处理CRC校验失败等错误 + AppLog.log('WiFi列表获取过程中发生错误: $error'); + + // 取消loading状态,显示错误提示 + dismissEasyLoading(); + cancelBlueConnetctToastTimer(); + + // 重置按钮状态,允许重新扫描 + state.sureBtnState.value = 0; + + // 如果是CRC校验失败,显示特定提示 + if (error.toString().contains('CRC')) { + showToast('数据校验失败,请重新扫描'.tr); + } else { + showToast('扫描WiFi失败,请重试'.tr); + } }); } - // 发送获取wifi列表数据解析 + /// 发送获取wifi列表数据解析 Future _replySendGetWifiParameters(Reply reply) async { final int status = reply.data[2]; switch (status) { case 0x00: - //成功 - showEasyLoading(); + //成功 - 不显示loading框,UI中已经有进度指示器 cancelBlueConnetctToastTimer(); - Future.delayed(5.seconds, dismissEasyLoading); break; case 0x06: // 需要鉴权 + dismissEasyLoading(); + AppLog.log('需要设备鉴权,请重试'.tr); + state.sureBtnState.value = 0; break; default: + // 处理其他错误状态 + dismissEasyLoading(); + AppLog.log('获取WiFi列表失败,错误码:$status'.tr); + state.sureBtnState.value = 0; break; } } - // 获取WiFi数据解析 + /// 获取WiFi数据解析 Future _replyGetWifiListParameters(Reply reply) async { final int status = reply.data[2]; switch (status) { case 0x00: //成功 - // showEasyLoading(); dismissEasyLoading(); state.sureBtnState.value = 0; if (reply.data[3] > 0) { reply.data.removeRange(0, 4); - // 把得到的数据按33位分割成数组 然后塞进一个新的数组里面 + // 把得到的数据按33位分割成数组然后处理 final List> getList = splitList(reply.data, 33); final List> uploadList = >[]; + for (int i = 0; i < getList.length; i++) { final List indexList = getList[i]; final Map indexMap = {}; final List wifiName = indexList.sublist(0, 32); - indexMap['wifiName'] = utf8String(wifiName); + final String wifiNameStr = utf8String(wifiName).trim(); + + // 过滤掉空的WiFi名称 + if (wifiNameStr.isEmpty) { + continue; + } + + indexMap['wifiName'] = wifiNameStr; indexMap['rssi'] = (indexList.last - 255).toString(); uploadList.add(indexMap); - state.wifiNameDataList.value = uploadList; } + + // 按信号强度排序WiFi列表 (从强到弱) + uploadList.sort((a, b) => + int.parse(b['rssi']!).compareTo(int.parse(a['rssi']!))); + + state.wifiNameDataList.value = uploadList; + + if (uploadList.isEmpty) { + showToast('未检测到可用的WiFi网络'.tr); + } + } else { + // 处理WiFi列表为空的情况 + state.wifiNameDataList.clear(); + showToast('未检测到可用的WiFi网络'.tr); } break; default: + // 处理其他错误状态 + dismissEasyLoading(); + showToast('解析WiFi列表失败,错误码:$status'.tr); + state.sureBtnState.value = 0; break; } } - // 获取wifi列表 + /// 获取WiFi列表 Future senderGetWifiListWifiAction() async { if (state.sureBtnState.value == 1) { return; } state.sureBtnState.value = 1; + state.wifiNameDataList.clear(); // 清空之前的列表 - showEasyLoading(); + // 不显示loading框,UI中已经有进度指示器 showBlueConnetctToastTimer(action: () { - dismissEasyLoading(); state.sureBtnState.value = 0; }); + BlueManage().blueSendData(BlueManage().connectDeviceName, (BluetoothConnectionState connectionState) async { if (connectionState == BluetoothConnectionState.connected) { - IoSenderManage.gatewayGetWifiCommand( - userID: await Storage.getUid(), - ); + try { + IoSenderManage.gatewayGetWifiCommand( + userID: await Storage.getUid(), + ); + } catch (e) { + state.sureBtnState.value = 0; + cancelBlueConnetctToastTimer(); + showToast('发送获取WiFi列表请求失败:${e.toString()}'.tr); + } } else if (connectionState == BluetoothConnectionState.disconnected) { - dismissEasyLoading(); state.sureBtnState.value = 0; cancelBlueConnetctToastTimer(); if (state.ifCurrentScreen.value == true) { @@ -113,22 +168,26 @@ class WifiListLogic extends BaseGetXController { } @override - void onReady() { + void onReady() async { super.onReady(); - _initReplySubscription(); + await senderGetWifiListWifiAction(); } @override void onInit() { super.onInit(); - - senderGetWifiListWifiAction(); + // 页面进入时标记为当前页面 + state.ifCurrentScreen.value = true; } @override void onClose() { - super.onClose(); + // 取消所有计时器和订阅,防止内存泄漏 _replySubscription.cancel(); + _connectionTimer?.cancel(); + cancelBlueConnetctToastTimer(); + state.ifCurrentScreen.value = false; + super.onClose(); } } diff --git a/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_page.dart b/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_page.dart index 2508cc27..fd4c6765 100755 --- a/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_page.dart +++ b/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_page.dart @@ -23,6 +23,32 @@ class _WifiListPageState extends State { final WifiListLogic logic = Get.put(WifiListLogic()); final WifiListState state = Get.find().state; + /// 计算WiFi信号强度图标 + IconData _getWifiSignalIcon(String rssi) { + final int rssiValue = int.parse(rssi); + if (rssiValue >= -50) { + return Icons.signal_wifi_4_bar; + } else if (rssiValue >= -70) { + return Icons.network_wifi; + } else if (rssiValue >= -80) { + return Icons.network_wifi_2_bar_rounded; + } else { + return Icons.signal_wifi_0_bar; + } + } + + @override + void initState() { + super.initState(); + state.ifCurrentScreen.value = true; + } + + @override + void dispose() { + state.ifCurrentScreen.value = false; + super.dispose(); + } + @override Widget build(BuildContext context) { return WillPopScope( @@ -38,13 +64,15 @@ class _WifiListPageState extends State { barTitle: 'WIFI列表'.tr, haveBack: state.pageName.value == 'lockSet', actionsList: [ - TextButton( - child: Text( - '刷新'.tr, - style: TextStyle(color: Colors.white, fontSize: 24.sp), - ), - onPressed: logic.senderGetWifiListWifiAction, - ), + Obx(() => TextButton( + child: Text( + '刷新'.tr, + style: TextStyle(color: Colors.white, fontSize: 24.sp), + ), + onPressed: state.sureBtnState.value == 0 + ? logic.senderGetWifiListWifiAction + : null, + )), ], backgroundColor: AppColors.mainColor, ), @@ -52,26 +80,56 @@ class _WifiListPageState extends State { children: [ Expanded( child: Obx(() => state.wifiNameDataList.value.isNotEmpty - ? ListView.builder( - itemCount: state.wifiNameDataList.value.length, - itemBuilder: (BuildContext c, int index) { - Map wifiNameStr = state.wifiNameDataList.value[index]; - return _messageListItem( - wifiNameStr['wifiName'], wifiNameStr['rssi'], () { - Get.toNamed(Routers.configuringWifiPage, - arguments: { - 'lockSetInfoData': - state.lockSetInfoData.value, - 'wifiName': wifiNameStr['wifiName'], - 'pageName': state.pageName.value, - }); - }); - }) - : NoData( - noDataHeight: 1.sh - - ScreenUtil().statusBarHeight - - ScreenUtil().bottomBarHeight - - 64.h)), + ? RefreshIndicator( + onRefresh: () async { + if (state.sureBtnState.value == 0) { + await logic.senderGetWifiListWifiAction(); + } + }, + child: ListView.builder( + itemCount: state.wifiNameDataList.value.length, + itemBuilder: (BuildContext c, int index) { + Map wifiNameStr = + state.wifiNameDataList.value[index]; + return _messageListItem( + wifiNameStr['wifiName'], wifiNameStr['rssi'], + () { + Get.toNamed(Routers.configuringWifiPage, + arguments: { + 'lockSetInfoData': + state.lockSetInfoData.value, + 'wifiName': wifiNameStr['wifiName'], + 'pageName': state.pageName.value, + }); + }); + }), + ) + : Obx(() => state.sureBtnState.value == 1 + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 50.w, + height: 50.w, + child: CircularProgressIndicator( + strokeWidth: 4.w, + valueColor: AlwaysStoppedAnimation( + AppColors.mainColor, + ), + backgroundColor: Colors.grey[200], + ), + ), + SizedBox(height: 20.h), + Text('正在扫描WiFi网络...\n请确保设备处于正常状态'.tr, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24.sp, + color: AppColors.blackColor)) + ], + ), + ) + : NoData())), ), state.pageName.value == 'saveLock' ? SubmitBtn( @@ -140,24 +198,37 @@ class _WifiListPageState extends State { height: 79.h, width: 1.sw - 20.w * 2, child: Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( + flex: 4, child: Text( - '$wifiName(${rssi}db)', + wifiName, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( - fontSize: 22.sp, color: AppColors.blackColor), + fontSize: 24.sp, color: AppColors.blackColor), ), ), - // Text( - // rssi, - // maxLines: 1, - // overflow: TextOverflow.ellipsis, - // style: TextStyle( - // fontSize: 22.sp, color: AppColors.blackColor), - // ) + Flexible( + flex: 1, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Icon( + _getWifiSignalIcon(rssi), + color: AppColors.mainColor, + size: 24.sp, + ), + SizedBox(width: 8.w), + Text( + '$rssi dB', + style: + TextStyle(fontSize: 18.sp, color: Colors.black), + ), + ], + ), + ) ], ), ), From 113803d97ddcf6faa306637c86813f3e6e03b56a Mon Sep 17 00:00:00 2001 From: liyi Date: Sat, 26 Apr 2025 15:15:46 +0800 Subject: [PATCH 035/151] =?UTF-8?q?fix:=E6=B3=A8=E9=87=8ACRC=E9=AA=8C?= =?UTF-8?q?=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/blue/reciver_data.dart | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/blue/reciver_data.dart b/lib/blue/reciver_data.dart index 2fc7fe54..0b0f4c61 100755 --- a/lib/blue/reciver_data.dart +++ b/lib/blue/reciver_data.dart @@ -61,16 +61,16 @@ class CommandReciverManager { final int dataSize = data.length; // 验证CRC校验 - if (dataSize >= 2) { - final int calculatedCrc = - _calculateCRC16(data.sublist(0, dataSize - 2), dataSize - 2); - final int receivedCrc = (data[dataSize - 2] << 8) | data[dataSize - 1]; - - if (calculatedCrc != receivedCrc) { - AppLog.log('CRC校验失败'); - return; - } - } + // if (dataSize >= 2) { + // final int calculatedCrc = + // _calculateCRC16(data.sublist(0, dataSize - 2), dataSize - 2); + // final int receivedCrc = (data[dataSize - 2] << 8) | data[dataSize - 1]; + // + // if (calculatedCrc != receivedCrc) { + // throw Exception('CRC校验失败'); + // return; + // } + // } // 当小于包头加起来13个字节 if (dataSize < 13) { return; From 61316051de96a726690c98814c1e1e5cddc0e130 Mon Sep 17 00:00:00 2001 From: liyi Date: Sat, 26 Apr 2025 15:16:02 +0800 Subject: [PATCH 036/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E7=A6=81?= =?UTF-8?q?=E7=94=A8=E6=8C=89=E9=92=AE=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/tools/submitBtn.dart | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/tools/submitBtn.dart b/lib/tools/submitBtn.dart index 01511077..81a57c62 100755 --- a/lib/tools/submitBtn.dart +++ b/lib/tools/submitBtn.dart @@ -53,15 +53,11 @@ class SubmitBtn extends StatelessWidget { ), child: ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: isDisabled == false + backgroundColor: isDisabled == true ? AppColors.btnDisableColor : (isDelete == true ? Colors.red : AppColors.mainColor), ), - onPressed: () { - if (onClick != null) { - onClick!(); - } - }, + onPressed: isDisabled == true ? null : onClick, child: Text( btnName!, textAlign: TextAlign.center, // 文本居中对齐 From 5f079fb3d422cfc6858adda6df20f1c6e9ed69bd Mon Sep 17 00:00:00 2001 From: liyi Date: Sat, 26 Apr 2025 15:16:39 +0800 Subject: [PATCH 037/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E8=93=9D?= =?UTF-8?q?=E7=89=99=E5=86=99=E5=85=A5=E9=80=BB=E8=BE=91=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E9=87=8D=E8=AF=95=E6=9C=BA=E5=88=B6=EF=BC=9B=E5=A4=84?= =?UTF-8?q?=E7=90=86GATT=E9=94=99=E8=AF=AF133?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/blue/blue_manage.dart | 60 ++++++++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/lib/blue/blue_manage.dart b/lib/blue/blue_manage.dart index 359758b9..70d9bc45 100755 --- a/lib/blue/blue_manage.dart +++ b/lib/blue/blue_manage.dart @@ -775,7 +775,7 @@ class BlueManage { } } - // 写入 + /// 写入蓝牙特征值,并等待响应 Future writeCharacteristicWithResponse(List value) async { final List services = await bluetoothConnectDevice!.discoverServices(); @@ -785,30 +785,64 @@ class BlueManage { in service.characteristics) { if (characteristic.characteristicUuid == _characteristicIdWrite) { try { + // 添加重试机制 + int retryCount = 0; + const int maxRetries = 3; + const int retryDelayMs = 500; + final List valueList = value; final List subData = splitList(valueList, _mtuSize!); - // AppLog.log('writeCharacteristicWithResponse _mtuSize:$_mtuSize 得到的分割数据:$subData'); + for (int i = 0; i < subData.length; i++) { - if (characteristic.properties.writeWithoutResponse) { - // 使用WRITE_NO_RESPONSE属性写入值 - await characteristic.write(subData[i], withoutResponse: true); - } else if (characteristic.properties.write) { - // 使用WRITE属性写入值 - await characteristic.write(subData[i]); - } else { - // 特性不支持写入 - throw Exception( - 'This characteristic does not support writing.'); + // 对每个数据包都应用重试逻辑 + bool packetSent = false; + retryCount = 0; + + while (!packetSent && retryCount < maxRetries) { + try { + if (characteristic.properties.writeWithoutResponse) { + await characteristic.write(subData[i], withoutResponse: true); + } else if (characteristic.properties.write) { + await characteristic.write(subData[i]); + } else { + // 特性不支持写入 + throw Exception('This characteristic does not support writing.'); + } + + // 如果到这里没有异常,则包发送成功 + packetSent = true; + } catch (e) { + if (e.toString().contains('UNKNOWN_GATT_ERROR (133)') && retryCount < maxRetries - 1) { + // GATT错误133,尝试重试 + retryCount++; + AppLog.log('蓝牙写入失败(GATT 133),数据包 ${i+1}/${subData.length} 正在重试 $retryCount/$maxRetries...'); + await Future.delayed(Duration(milliseconds: retryDelayMs)); + continue; + } else { + // 其他错误或已达到最大重试次数,抛出异常 + AppLog.log('APP写入失败: $e'); + throw e; + } + } + } + + if (!packetSent) { + throw Exception('蓝牙写入失败,数据包 ${i+1}/${subData.length} 已达到最大重试次数'); } } + + return; // 所有数据包都发送成功 } on Exception catch (e, s) { - AppLog.log('APP写入失败: $e $s'); + AppLog.log('APP写入失败: $e $s'); rethrow; } } } } } + + // 如果找不到合适的特性用于写入 + throw Exception('未找到适合写入的蓝牙特性'); } // 停止扫描蓝牙设备 From 96367baaeaeef016dc1c49c1088e8ada06c63218 Mon Sep 17 00:00:00 2001 From: liyi Date: Sat, 26 Apr 2025 15:51:16 +0800 Subject: [PATCH 038/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E9=85=8D?= =?UTF-8?q?=E7=BD=91=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuringWifi_logic.dart | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart index a940ca4e..3b23c309 100755 --- a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart +++ b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart @@ -55,7 +55,7 @@ class ConfiguringWifiLogic extends BaseGetXController { } /// 更新网络信息到服务器 - void updateNetworkInfo({ + Future updateNetworkInfo({ required String peerId, required String wifiName, required String secretKey, @@ -71,27 +71,12 @@ class ConfiguringWifiLogic extends BaseGetXController { secretKey: secretKey, peerId: peerId, ); - - if (entity.errorCode!.codeIsSuccessful) { - // 设置锁的peerID - StartChartManage().lockNetworkInfo = DeviceNetworkInfo( - wifiName: wifiName, - networkMac: networkMac, - secretKey: secretKey, - peerId: peerId, - ); - - await _getUploadLockSet(); - } else { - dismissEasyLoading(); - showToast('网络配置失败,请重试'.tr); - state.sureBtnState.value = 0; - } + return entity; } catch (e) { dismissEasyLoading(); - showToast('网络配置异常:${e.toString()}'.tr); state.sureBtnState.value = 0; AppLog.log('网络配置异常:$e'); + return LoginEntity(); } } @@ -174,19 +159,34 @@ class ConfiguringWifiLogic extends BaseGetXController { throw Exception('Missing required network information'); } - /// 配网成功后,赋值锁的peerId - StartChartManage().lockPeerId = peerId; - - // 保存到缓存 - await Storage.saveLockNetWorkInfo(jsonMap); - // 上报服务器 - 注意: sureBtnState 状态将在 updateNetworkInfo 方法中或其回调中完成重置 - updateNetworkInfo( + final info = await updateNetworkInfo( peerId: peerId, wifiName: wifiName ?? '', secretKey: secretKey, deviceMac: deviceMac ?? '', networkMac: networkMac ?? ''); + if (info.errorCode!.codeIsSuccessful) { + // 设置锁的peerID + StartChartManage().lockNetworkInfo = DeviceNetworkInfo( + wifiName: wifiName, + networkMac: networkMac, + secretKey: secretKey, + peerId: peerId, + ); + + /// 配网成功后,赋值锁的peerId + StartChartManage().lockPeerId = peerId; + + // 保存到缓存 + await Storage.saveLockNetWorkInfo(jsonMap); + + await _getUploadLockSet(); + } else { + dismissEasyLoading(); + showToast('网络配置失败,请重试'.tr); + state.sureBtnState.value = 0; + } } catch (e) { if (EasyLoading.isShow) { dismissEasyLoading(); @@ -495,13 +495,13 @@ class ConfiguringWifiLogic extends BaseGetXController { throw Exception('Token is empty'); } final List getTokenList = changeStringListToIntList(token); + // 蓝牙获取锁设置 await _uploadLockSet(getTokenList); } catch (e) { if (EasyLoading.isShow) { dismissEasyLoading(); } cancelBlueConnetctToastTimer(); - showToast('获取设置失败:${e.toString()}'.tr); state.sureBtnState.value = 0; } } @@ -613,14 +613,14 @@ class ConfiguringWifiLogic extends BaseGetXController { if (EasyLoading.isShow) { dismissEasyLoading(); } - showToast('数据上传失败:${entity.errorCode}'.tr); + // showToast('数据上传失败:${entity.errorCode}'.tr); state.sureBtnState.value = 0; // 确保重置状态 } } catch (e) { if (EasyLoading.isShow) { dismissEasyLoading(); } - showToast('数据上传异常:${e.toString()}'.tr); + // showToast('数据上传异常:${e.toString()}'.tr); state.sureBtnState.value = 0; // 确保重置状态 } } From a09237bc021cecd949919ac523ce5192605b56f7 Mon Sep 17 00:00:00 2001 From: liyi Date: Sun, 27 Apr 2025 09:22:18 +0800 Subject: [PATCH 039/151] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E8=BF=9B?= =?UTF-8?q?=E5=85=A5=E6=8C=87=E7=BA=B9=E4=B8=8D=E8=87=AA=E5=8A=A8=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E6=8C=87=E7=BA=B9=E5=88=97=E8=A1=A8=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/main/lockDetail/card/cardList/cardList_logic.dart | 3 ++- lib/main/lockDetail/face/faceList/faceList_logic.dart | 3 ++- .../fingerprint/fingerprintList/fingerprintList_logic.dart | 1 + lib/main/lockDetail/palm/palmList/palmList_logic.dart | 1 + 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/main/lockDetail/card/cardList/cardList_logic.dart b/lib/main/lockDetail/card/cardList/cardList_logic.dart index 2fc2e376..c007eca1 100755 --- a/lib/main/lockDetail/card/cardList/cardList_logic.dart +++ b/lib/main/lockDetail/card/cardList/cardList_logic.dart @@ -249,8 +249,9 @@ class CardListLogic extends BaseGetXController { _initReplySubscription(); // _initRefreshAction(); + await getICCardListData(isRefresh: true); } - await getICCardListData(isRefresh: true); + } @override diff --git a/lib/main/lockDetail/face/faceList/faceList_logic.dart b/lib/main/lockDetail/face/faceList/faceList_logic.dart index a1499556..2f6e8a18 100755 --- a/lib/main/lockDetail/face/faceList/faceList_logic.dart +++ b/lib/main/lockDetail/face/faceList/faceList_logic.dart @@ -443,8 +443,9 @@ class FaceListLogic extends BaseGetXController { // senderCheckingCardStatus(); // senderCheckingUserInfoCount(); + await getFaceListData(isRefresh: true); } - getFaceListData(isRefresh: true); + } @override diff --git a/lib/main/lockDetail/fingerprint/fingerprintList/fingerprintList_logic.dart b/lib/main/lockDetail/fingerprint/fingerprintList/fingerprintList_logic.dart index d65269cd..dc5bb895 100755 --- a/lib/main/lockDetail/fingerprint/fingerprintList/fingerprintList_logic.dart +++ b/lib/main/lockDetail/fingerprint/fingerprintList/fingerprintList_logic.dart @@ -461,6 +461,7 @@ class FingerprintListLogic extends BaseGetXController { _initReplySubscription(); // _initRefreshAction(); + await getFingerprintsListData(isRefresh: true); } } diff --git a/lib/main/lockDetail/palm/palmList/palmList_logic.dart b/lib/main/lockDetail/palm/palmList/palmList_logic.dart index 8d15fa33..c4b94410 100755 --- a/lib/main/lockDetail/palm/palmList/palmList_logic.dart +++ b/lib/main/lockDetail/palm/palmList/palmList_logic.dart @@ -234,6 +234,7 @@ class PalmListLogic extends BaseGetXController { _initReplySubscription(); // _initRefreshAction(); + await getPalmListData(isRefresh: true); } } From 2de9d32b614c6e82db3fadd5dd3db20277b7d086 Mon Sep 17 00:00:00 2001 From: liyi Date: Sun, 27 Apr 2025 09:53:05 +0800 Subject: [PATCH 040/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E6=97=A0?= =?UTF-8?q?=E9=9F=B3=E9=A2=91=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handle/impl/udp_talk_accept_handler.dart | 1 + .../handle/impl/udp_talk_request_handler.dart | 51 +++++----- .../views/talkView/talk_view_logic.dart | 31 ++++-- .../starChart/webView/h264_web_logic.dart | 94 ++++++++++++++----- lib/talk/starChart/webView/h264_web_view.dart | 7 +- 5 files changed, 127 insertions(+), 57 deletions(-) diff --git a/lib/talk/starChart/handle/impl/udp_talk_accept_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_accept_handler.dart index 2605525c..baaae2e8 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_accept_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_accept_handler.dart @@ -78,6 +78,7 @@ class UdpTalkAcceptHandler extends ScpMessageBaseHandle } } + /// 收到同意接听回复之后增加音频的期望数据 void _handleSendExpect() { final LockListInfoItemEntity currentKeyInfo = CommonDataManage().currentKeyInfo; diff --git a/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart index 974c8f25..91fdcc83 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart @@ -1,20 +1,15 @@ import 'dart:convert'; import 'dart:io'; -import 'package:flutter/services.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/app_settings/app_settings.dart'; import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart'; import 'package:star_lock/talk/starChart/constant/message_type_constant.dart'; -import 'package:star_lock/talk/starChart/constant/talk_constant.dart'; import 'package:star_lock/talk/starChart/constant/talk_status.dart'; import 'package:star_lock/talk/starChart/entity/scp_message.dart'; import 'package:star_lock/talk/starChart/handle/scp_message_base_handle.dart'; import 'package:star_lock/talk/starChart/handle/scp_message_handle.dart'; -import 'package:star_lock/talk/starChart/proto/gateway_reset.pb.dart'; import 'package:star_lock/talk/starChart/proto/generic.pb.dart'; import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart'; import 'package:star_lock/talk/starChart/proto/talk_request.pb.dart'; @@ -28,26 +23,10 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle RxString currentLanguage = CurrentLocaleTool.getCurrentLocaleString().obs; // 当前选择语言 - // 添加上次处理请求的时间戳 - int _lastRequestTime = 0; - @override void handleReq(ScpMessage scpMessage) async { - final currentTime = DateTime.now().millisecondsSinceEpoch; - // 确保与上次请求间隔至少1秒 - if (currentTime - _lastRequestTime < 1000) { - // 如果间隔小于1秒,直接拒绝请求 - replyErrorMessage(scpMessage); - AppLog.log('对讲请求过于频繁,已拒绝'); - return; - } - - // 更新最后处理时间 - _lastRequestTime = currentTime; - // 判断是否登录账户 final loginData = await Storage.getLoginData(); - // 如果登录账户不为空,且不是被动接听状态,且不是接听成功状态 if (loginData != null && (talkStatus.status != TalkStatus.passiveCallWaitingAnswer || @@ -77,6 +56,8 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle // 收到对讲请求的应答 startChartManage.FromPeerId = scpMessage.ToPeerId!; startChartManage.ToPeerId = scpMessage.FromPeerId!; + // 处理预期数据格式 + _handleResponseSendExpect(); // 发送预期数据 startChartManage.startTalkExpectTimer(); // 停止发送对讲请求 @@ -99,7 +80,7 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle // 来电事件的处理 void _talkRequestEvent({required String talkObjectName}) { // 发送预期数据、通知锁板需要获取视频数据 - _handleSendExpect(); + _handleRequestSendExpect(); // 播放铃声 //test:使用自定义铃声 playRingtone(); @@ -188,7 +169,8 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle } } - void _handleSendExpect() { + /// app收到的对讲请求后,发送的预期数据 + void _handleRequestSendExpect() { final LockListInfoItemEntity currentKeyInfo = CommonDataManage().currentKeyInfo; final isH264 = currentKeyInfo.lockFeature?.isH264 == 1; @@ -209,4 +191,27 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle print('锁不支持H264和MJPEG,默认发送图像视频格式期望数据'); } } + + /// app主动发请求,收到回复后发送的预期数据 + void _handleResponseSendExpect() { + final LockListInfoItemEntity currentKeyInfo = + CommonDataManage().currentKeyInfo; + final isH264 = currentKeyInfo.lockFeature?.isH264 == 1; + final isMJpeg = currentKeyInfo.lockFeature?.isMJpeg == 1; + + // 优先使用H264,其次是MJPEG + if (isH264) { + // 锁支持H264,发送H264视频和G711音频期望 + startChartManage.sendH264VideoAndG711AudioTalkExpectData(); + print('锁支持H264,发送H264视频格式期望数据'); + } else if (isMJpeg) { + // 锁只支持MJPEG,发送图像视频和G711音频期望 + startChartManage.sendImageVideoAndG711AudioTalkExpectData(); + print('锁不支持H264,支持MJPEG,发送MJPEG视频格式期望数据'); + } else { + // 默认使用图像视频 + startChartManage.sendImageVideoAndG711AudioTalkExpectData(); + print('锁不支持H264和MJPEG,默认发送图像视频格式期望数据'); + } + } } diff --git a/lib/talk/starChart/views/talkView/talk_view_logic.dart b/lib/talk/starChart/views/talkView/talk_view_logic.dart index eaf051c1..cc959c0b 100644 --- a/lib/talk/starChart/views/talkView/talk_view_logic.dart +++ b/lib/talk/starChart/views/talkView/talk_view_logic.dart @@ -51,7 +51,6 @@ class TalkViewLogic extends BaseGetXController { int _startAudioTime = 0; // 开始播放时间戳 bool _isFirstFrame = true; // 是否是第一帧 - // 定义音频帧缓冲和发送函数 final List _bufferedAudioFrames = []; @@ -65,6 +64,10 @@ class TalkViewLogic extends BaseGetXController { int _lastFpsUpdateTime = 0; Timer? _fpsTimer; + // 添加监听状态和订阅引用 + bool _isListening = false; + StreamSubscription? _streamSubscription; + /// 初始化音频播放器 void _initFlutterPcmSound() { const int sampleRate = 8000; @@ -97,7 +100,15 @@ class TalkViewLogic extends BaseGetXController { // 监听音视频数据流 void _startListenTalkData() { - state.talkDataRepository.talkDataStream + // 防止重复监听 + if (_isListening) { + AppLog.log("已经存在数据流监听,避免重复监听"); + return; + } + + AppLog.log("==== 启动新的数据流监听 ===="); + _isListening = true; + _streamSubscription = state.talkDataRepository.talkDataStream .listen((TalkDataModel talkDataModel) async { final talkData = talkDataModel.talkData; final contentType = talkData!.contentType; @@ -106,13 +117,13 @@ class TalkViewLogic extends BaseGetXController { // 判断数据类型,进行分发处理 switch (contentType) { case TalkData_ContentTypeE.G711: - // // 第一帧到达时记录开始时间 - if (_isFirstAudioFrame) { - _startAudioTime = currentTime; - _isFirstAudioFrame = false; - } + // // 第一帧到达时记录开始时间 + if (_isFirstAudioFrame) { + _startAudioTime = currentTime; + _isFirstAudioFrame = false; + } - // 计算音频延迟 + // 计算音频延迟 final expectedTime = _startAudioTime + talkData.durationMs; final audioDelay = currentTime - expectedTime; @@ -384,7 +395,6 @@ class TalkViewLogic extends BaseGetXController { } } - /// 获取权限状态 Future getPermissionStatus() async { final Permission permission = Permission.microphone; @@ -504,6 +514,9 @@ class TalkViewLogic extends BaseGetXController { state.oneMinuteTimeTimer?.cancel(); // 取消旧定时器 state.oneMinuteTimeTimer = null; // 取消旧定时器 state.oneMinuteTime.value = 0; + // 取消数据流监听 + _streamSubscription?.cancel(); + _isListening = false; super.onClose(); } diff --git a/lib/talk/starChart/webView/h264_web_logic.dart b/lib/talk/starChart/webView/h264_web_logic.dart index 4f5789c2..b7307383 100644 --- a/lib/talk/starChart/webView/h264_web_logic.dart +++ b/lib/talk/starChart/webView/h264_web_logic.dart @@ -54,6 +54,10 @@ class H264WebViewLogic extends BaseGetXController { final Queue> _frameBuffer = Queue>(); static const int FRAME_BUFFER_SIZE = 25; + // 添加监听状态和订阅引用 + bool _isListening = false; + StreamSubscription? _streamSubscription; + @override void onInit() { // 初始化 WebView 控制器 @@ -122,7 +126,15 @@ class H264WebViewLogic extends BaseGetXController { } void _createFramesStreamListen() async { - state.talkDataRepository.talkDataStream + // 防止重复监听 + if (_isListening) { + AppLog.log("已经存在数据流监听,避免重复监听"); + return; + } + + AppLog.log("==== 启动新的数据流监听 ===="); + _isListening = true; + _streamSubscription = state.talkDataRepository.talkDataStream .listen((TalkDataModel talkDataModel) async { final talkData = talkDataModel.talkData; final contentType = talkData!.contentType; @@ -131,30 +143,33 @@ class H264WebViewLogic extends BaseGetXController { // 判断数据类型,进行分发处理 switch (contentType) { case TalkData_ContentTypeE.G711: - // // 第一帧到达时记录开始时间 - if (_isFirstAudioFrame) { - _startAudioTime = currentTime; - _isFirstAudioFrame = false; - } - - // 计算音频延迟 - final expectedTime = _startAudioTime + talkData.durationMs; - final audioDelay = currentTime - expectedTime; - - // 如果延迟太大,清空缓冲区并直接播放 - if (audioDelay > 500) { - state.audioBuffer.clear(); - if (state.isOpenVoice.value) { - _playAudioFrames(); + if (state.isShowLoading.isFalse) { + // // 第一帧到达时记录开始时间 + if (_isFirstAudioFrame) { + _startAudioTime = currentTime; + _isFirstAudioFrame = false; } - return; + + // 计算音频延迟 + final expectedTime = _startAudioTime + talkData.durationMs; + final audioDelay = currentTime - expectedTime; + + // 如果延迟太大,清空缓冲区并直接播放 + if (audioDelay > 500) { + state.audioBuffer.clear(); + if (state.isOpenVoice.value) { + _playAudioFrames(); + } + return; + } + if (state.audioBuffer.length >= audioBufferSize) { + state.audioBuffer.removeAt(0); // 丢弃最旧的数据 + } + state.audioBuffer.add(talkData); // 添加新数据 + // 添加音频播放逻辑,与视频类似 + _playAudioFrames(); } - if (state.audioBuffer.length >= audioBufferSize) { - state.audioBuffer.removeAt(0); // 丢弃最旧的数据 - } - state.audioBuffer.add(talkData); // 添加新数据 - // 添加音频播放逻辑,与视频类似 - _playAudioFrames(); + break; case TalkData_ContentTypeE.H264: // // 添加新帧到缓冲区 @@ -537,6 +552,39 @@ class H264WebViewLogic extends BaseGetXController { } } + /// 停止播放音频 + void _stopPlayG711Data() async { + await FlutterPcmSound.pause(); + await FlutterPcmSound.stop(); + await FlutterPcmSound.clear(); + } + + @override + void onClose() { + _stopPlayG711Data(); // 停止播放音频 + + state.audioBuffer.clear(); // 清空音频缓冲区 + + state.oneMinuteTimeTimer?.cancel(); + state.oneMinuteTimeTimer = null; + + // 停止播放音频 + stopProcessingAudio(); + + state.oneMinuteTimeTimer?.cancel(); // 取消旧定时器 + state.oneMinuteTimeTimer = null; // 取消旧定时器 + state.oneMinuteTime.value = 0; + + // 取消数据流监听 + _streamSubscription?.cancel(); + _isListening = false; + + // 重置期望数据 + StartChartManage().reSetDefaultTalkExpect(); + + super.onClose(); + } + @override void dispose() { // _mockDataTimer?.cancel(); diff --git a/lib/talk/starChart/webView/h264_web_view.dart b/lib/talk/starChart/webView/h264_web_view.dart index 51aa30d8..aa18aa3c 100644 --- a/lib/talk/starChart/webView/h264_web_view.dart +++ b/lib/talk/starChart/webView/h264_web_view.dart @@ -7,6 +7,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; import 'package:star_lock/app_settings/app_colors.dart'; import 'package:star_lock/app_settings/app_settings.dart'; +import 'package:star_lock/talk/call/callTalk.dart'; import 'package:star_lock/talk/starChart/constant/talk_status.dart'; import 'package:star_lock/talk/starChart/handle/other/talk_data_repository.dart'; import 'package:star_lock/talk/starChart/proto/talk_data.pbserver.dart'; @@ -413,8 +414,10 @@ class _H264WebViewState extends State } @override void dispose() { - state.animationController.dispose(); // 确保释放控制器 - super.dispose(); + state.animationController.dispose(); + CallTalk().finishAVData(); + // UdpTalkDataHandler().resetDataRates(); + super.dispose(); } } From db2fe012d70f7a4aa12c971a6ae0e5686ad20f5c Mon Sep 17 00:00:00 2001 From: liyi Date: Sun, 27 Apr 2025 10:12:34 +0800 Subject: [PATCH 041/151] =?UTF-8?q?fix:=E9=80=80=E5=87=BA=E5=90=8E?= =?UTF-8?q?=E5=8F=B0=E6=97=B6=E8=87=AA=E5=8A=A8=E6=8C=82=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/talk/starChart/star_chart_manage.dart | 11 ++++++++++- lib/talk/starChart/status/appLifecycle_observer.dart | 3 +-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/talk/starChart/star_chart_manage.dart b/lib/talk/starChart/star_chart_manage.dart index 66de4aeb..1fb79272 100644 --- a/lib/talk/starChart/star_chart_manage.dart +++ b/lib/talk/starChart/star_chart_manage.dart @@ -1199,6 +1199,16 @@ class StartChartManage { /// 销毁资源 void destruction() async { + // 先挂断 + final status = talkStatus.status; + if (status == TalkStatus.passiveCallWaitingAnswer || + status == TalkStatus.proactivelyCallWaitingAnswer || + status == TalkStatus.answeredSuccessfully || + status == TalkStatus.uninitialized) { + startTalkRejectMessageTimer(); + startTalkHangupMessageTimer(); + await Future.delayed(Duration(seconds: 1)); + } isOnlineStarChartServer = false; // 停止发送心跳消息 stopHeartbeat(); @@ -1226,7 +1236,6 @@ class StartChartManage { await Storage.removerStarChartRegisterNodeInfo(); // 关闭udp服务 closeUdpSocket(); - } /// 重置数据 diff --git a/lib/talk/starChart/status/appLifecycle_observer.dart b/lib/talk/starChart/status/appLifecycle_observer.dart index 37b70152..03a356d1 100644 --- a/lib/talk/starChart/status/appLifecycle_observer.dart +++ b/lib/talk/starChart/status/appLifecycle_observer.dart @@ -30,14 +30,13 @@ class AppLifecycleObserver extends WidgetsBindingObserver { // 处理应用程序进入后台的逻辑 final status = StartChartManage().talkStatus.status; - if (status == TalkStatus.passiveCallWaitingAnswer || status == TalkStatus.proactivelyCallWaitingAnswer || status == TalkStatus.answeredSuccessfully || status == TalkStatus.uninitialized) { - StartChartManage().destruction(); Get.back(); } + StartChartManage().destruction(); } void onAppResumed() async { From ea96419e05d2f0911b872d13cc4fdabf70290384 Mon Sep 17 00:00:00 2001 From: liyi Date: Sun, 27 Apr 2025 11:27:31 +0800 Subject: [PATCH 042/151] =?UTF-8?q?fix:=E5=9B=9E=E5=A4=8D=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E5=8E=9F=E6=9C=89=E7=9A=84=E7=A6=81=E7=94=A8=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/tools/submitBtn.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/tools/submitBtn.dart b/lib/tools/submitBtn.dart index 81a57c62..01511077 100755 --- a/lib/tools/submitBtn.dart +++ b/lib/tools/submitBtn.dart @@ -53,11 +53,15 @@ class SubmitBtn extends StatelessWidget { ), child: ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: isDisabled == true + backgroundColor: isDisabled == false ? AppColors.btnDisableColor : (isDelete == true ? Colors.red : AppColors.mainColor), ), - onPressed: isDisabled == true ? null : onClick, + onPressed: () { + if (onClick != null) { + onClick!(); + } + }, child: Text( btnName!, textAlign: TextAlign.center, // 文本居中对齐 From d75754e14e41322e16385925943b45ae864dc1d2 Mon Sep 17 00:00:00 2001 From: liyi Date: Mon, 28 Apr 2025 09:20:11 +0800 Subject: [PATCH 043/151] =?UTF-8?q?fix:SKY=E7=8E=AF=E5=A2=83=E4=B8=8B?= =?UTF-8?q?=E6=B3=A8=E9=87=8A=E8=AF=A5=E9=94=81=E5=B7=B2=E8=A2=AB=E9=87=8D?= =?UTF-8?q?=E7=BD=AE=E7=9A=84=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/blue/blue_manage.dart | 42 +++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/lib/blue/blue_manage.dart b/lib/blue/blue_manage.dart index 70d9bc45..064fe984 100755 --- a/lib/blue/blue_manage.dart +++ b/lib/blue/blue_manage.dart @@ -5,6 +5,7 @@ import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:get/get.dart'; import 'package:star_lock/app_settings/app_settings.dart'; +import 'package:star_lock/flavors.dart'; import 'package:star_lock/tools/bugly/bugly_tool.dart'; import 'package:star_lock/tools/commonDataManage.dart'; @@ -551,7 +552,10 @@ class BlueManage { }); } else { connectStateCallBack(BluetoothConnectionState.disconnected); - EasyLoading.showToast('该锁已被重置'.tr, duration: 2000.milliseconds); + if (!F.isSKY) { + EasyLoading.showToast('该锁已被重置'.tr, duration: 2000.milliseconds); + } + scanDevices.clear(); BuglyTool.uploadException( @@ -582,7 +586,9 @@ class BlueManage { }); } else { connectStateCallBack(BluetoothConnectionState.disconnected); - EasyLoading.showToast('该锁已被重置'.tr, duration: 2000.milliseconds); + if (!F.isSKY) { + EasyLoading.showToast('该锁已被重置'.tr, duration: 2000.milliseconds); + } scanDevices.clear(); BuglyTool.uploadException( @@ -789,34 +795,39 @@ class BlueManage { int retryCount = 0; const int maxRetries = 3; const int retryDelayMs = 500; - + final List valueList = value; final List subData = splitList(valueList, _mtuSize!); - + for (int i = 0; i < subData.length; i++) { // 对每个数据包都应用重试逻辑 bool packetSent = false; retryCount = 0; - + while (!packetSent && retryCount < maxRetries) { try { if (characteristic.properties.writeWithoutResponse) { - await characteristic.write(subData[i], withoutResponse: true); + await characteristic.write(subData[i], + withoutResponse: true); } else if (characteristic.properties.write) { await characteristic.write(subData[i]); } else { // 特性不支持写入 - throw Exception('This characteristic does not support writing.'); + throw Exception( + 'This characteristic does not support writing.'); } - + // 如果到这里没有异常,则包发送成功 packetSent = true; } catch (e) { - if (e.toString().contains('UNKNOWN_GATT_ERROR (133)') && retryCount < maxRetries - 1) { + if (e.toString().contains('UNKNOWN_GATT_ERROR (133)') && + retryCount < maxRetries - 1) { // GATT错误133,尝试重试 retryCount++; - AppLog.log('蓝牙写入失败(GATT 133),数据包 ${i+1}/${subData.length} 正在重试 $retryCount/$maxRetries...'); - await Future.delayed(Duration(milliseconds: retryDelayMs)); + AppLog.log( + '蓝牙写入失败(GATT 133),数据包 ${i + 1}/${subData.length} 正在重试 $retryCount/$maxRetries...'); + await Future.delayed( + Duration(milliseconds: retryDelayMs)); continue; } else { // 其他错误或已达到最大重试次数,抛出异常 @@ -825,12 +836,13 @@ class BlueManage { } } } - + if (!packetSent) { - throw Exception('蓝牙写入失败,数据包 ${i+1}/${subData.length} 已达到最大重试次数'); + throw Exception( + '蓝牙写入失败,数据包 ${i + 1}/${subData.length} 已达到最大重试次数'); } } - + return; // 所有数据包都发送成功 } on Exception catch (e, s) { AppLog.log('APP写入失败: $e $s'); @@ -840,7 +852,7 @@ class BlueManage { } } } - + // 如果找不到合适的特性用于写入 throw Exception('未找到适合写入的蓝牙特性'); } From ea01d79bbdc652d0d6c5bd1243c68eab248c611c Mon Sep 17 00:00:00 2001 From: liyi Date: Mon, 28 Apr 2025 09:20:25 +0800 Subject: [PATCH 044/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E9=85=8D?= =?UTF-8?q?=E7=BD=91=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuringWifi_logic.dart | 164 +++++++----------- .../configuringWifi/configuringWifi_page.dart | 2 +- .../wifiList/wifiList_logic.dart | 25 +-- 3 files changed, 81 insertions(+), 110 deletions(-) diff --git a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart index 3b23c309..76cc2bf4 100755 --- a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart +++ b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart @@ -181,17 +181,28 @@ class ConfiguringWifiLogic extends BaseGetXController { // 保存到缓存 await Storage.saveLockNetWorkInfo(jsonMap); - await _getUploadLockSet(); + showToast('配网成功'.tr, something: () { + state.sureBtnState.value = 0; // 确保重置状态 + if (state.pageName.value == 'lockSet') { + Get.close(2); + } else { + Get.offAllNamed(Routers.starLockMain); + } + eventBus.fire(SuccessfulDistributionNetwork()); + }); + + // 获取锁设置 + _getUploadLockSet(); } else { dismissEasyLoading(); - showToast('网络配置失败,请重试'.tr); + // showToast('网络配置失败,请重试'.tr); state.sureBtnState.value = 0; } } catch (e) { if (EasyLoading.isShow) { dismissEasyLoading(); } - showToast('解析配网信息失败,请重试'.tr); + // showToast('解析配网信息失败,请重试'.tr); state.sureBtnState.value = 0; // 确保重置状态 AppLog.log('解析配网信息失败: $e'); return; // 添加return阻止后续流程 @@ -203,7 +214,7 @@ class ConfiguringWifiLogic extends BaseGetXController { if (EasyLoading.isShow) { dismissEasyLoading(); } - showToast('WiFi密码错误,请重新输入'.tr); + // showToast('WiFi密码错误,请重新输入'.tr); state.sureBtnState.value = 0; // 确保重置状态 break; @@ -212,7 +223,7 @@ class ConfiguringWifiLogic extends BaseGetXController { if (EasyLoading.isShow) { dismissEasyLoading(); } - showToast('找不到该WiFi网络,请确认WiFi名称正确'.tr); + // showToast('找不到该WiFi网络,请确认WiFi名称正确'.tr); state.sureBtnState.value = 0; // 确保重置状态 break; @@ -221,7 +232,7 @@ class ConfiguringWifiLogic extends BaseGetXController { if (EasyLoading.isShow) { dismissEasyLoading(); } - showToast('连接WiFi超时,请确保网络信号良好'.tr); + // showToast('连接WiFi超时,请确保网络信号良好'.tr); state.sureBtnState.value = 0; // 确保重置状态 break; @@ -230,7 +241,7 @@ class ConfiguringWifiLogic extends BaseGetXController { if (EasyLoading.isShow) { dismissEasyLoading(); } - showToast('配网失败 (错误码: $status),请重试'.tr); + // showToast('配网失败 (错误码: $status),请重试'.tr); state.sureBtnState.value = 0; // 确保重置状态 break; } @@ -315,12 +326,15 @@ class ConfiguringWifiLogic extends BaseGetXController { } // 设置蓝牙操作超时处理 - showBlueConnetctToastTimer(action: () { - if (EasyLoading.isShow) { - dismissEasyLoading(); - } - state.sureBtnState.value = 0; // 连接超时时重置状态 - }); + showBlueConnetctToastTimer( + action: () { + if (EasyLoading.isShow) { + dismissEasyLoading(); + } + state.sureBtnState.value = 0; // 连接超时时重置状态 + }, + outTimer: 30, + ); // 发送配网指令 BlueManage().blueSendData( @@ -340,7 +354,7 @@ class ConfiguringWifiLogic extends BaseGetXController { } cancelBlueConnetctToastTimer(); state.sureBtnState.value = 0; // 发送命令失败时重置状态 - showToast('发送配网指令失败:${e.toString()}'.tr); + // showToast('发送配网指令失败:${e.toString()}'.tr); } } else if (connectionState == BluetoothConnectionState.disconnected) { if (EasyLoading.isShow) { @@ -474,70 +488,53 @@ class ConfiguringWifiLogic extends BaseGetXController { default: //失败 dismissEasyLoading(); - showToast('获取设备状态失败'.tr); + // showToast('获取设备状态失败'.tr); break; } } // 上传数据获取设置 Future _getUploadLockSet() async { - // 保持已有的loading状态,不再重新显示loading - showBlueConnetctToastTimer(action: () { - if (EasyLoading.isShow) { - dismissEasyLoading(); - } - state.sureBtnState.value = 0; - }); - - try { - final List? token = await Storage.getStringList(saveBlueToken); - if (token == null || token.isEmpty) { - throw Exception('Token is empty'); - } - final List getTokenList = changeStringListToIntList(token); - // 蓝牙获取锁设置 - await _uploadLockSet(getTokenList); - } catch (e) { - if (EasyLoading.isShow) { - dismissEasyLoading(); - } - cancelBlueConnetctToastTimer(); - state.sureBtnState.value = 0; - } + final List? token = await Storage.getStringList(saveBlueToken); + final List getTokenList = changeStringListToIntList(token!); + // 蓝牙获取锁设置 + await _uploadLockSet(getTokenList); } // 公共的上传锁设置 Future _uploadLockSet(List token) async { - try { - final List? privateKey = - await Storage.getStringList(saveBluePrivateKey); - if (privateKey == null || privateKey.isEmpty) { - throw Exception('Private key is empty'); - } - final List getPrivateKeyList = changeStringListToIntList(privateKey); + final List? privateKey = + await Storage.getStringList(saveBluePrivateKey); + if (privateKey == null || privateKey.isEmpty) { + throw Exception('Private key is empty'); + } + final List getPrivateKeyList = changeStringListToIntList(privateKey); - final List? signKey = - await Storage.getStringList(saveBlueSignKey); - if (signKey == null || signKey.isEmpty) { - throw Exception('Sign key is empty'); - } - final List signKeyDataList = changeStringListToIntList(signKey); + final List? signKey = await Storage.getStringList(saveBlueSignKey); + if (signKey == null || signKey.isEmpty) { + throw Exception('Sign key is empty'); + } + final List signKeyDataList = changeStringListToIntList(signKey); - IoSenderManage.updataLockSetCommand( + BlueManage().blueSendData(BlueManage().connectDeviceName, + (BluetoothConnectionState connectionState) async { + if (connectionState == BluetoothConnectionState.connected) { + IoSenderManage.updataLockSetCommand( lockID: BlueManage().connectDeviceName, userID: await Storage.getUid(), token: token, needAuthor: 1, signKey: signKeyDataList, - privateKey: getPrivateKeyList); - } catch (e) { - if (EasyLoading.isShow) { + privateKey: getPrivateKeyList, + ); + } else if (connectionState == BluetoothConnectionState.disconnected) { dismissEasyLoading(); + cancelBlueConnetctToastTimer(); + if (state.ifCurrentScreen.value == true) { + showBlueConnetctToast(); + } } - cancelBlueConnetctToastTimer(); - showToast('上传设置失败:${e.toString()}'.tr); - state.sureBtnState.value = 0; - } + }, isAddEquipment: true); } // 上传数据获取锁设置解析 @@ -565,7 +562,7 @@ class ConfiguringWifiLogic extends BaseGetXController { if (EasyLoading.isShow) { dismissEasyLoading(); // 错误时关闭loading } - showToast('获取设置权限失败:${e.toString()}'.tr); + // showToast('获取设置权限失败:${e.toString()}'.tr); state.sureBtnState.value = 0; // 确保重置状态 } break; @@ -574,7 +571,7 @@ class ConfiguringWifiLogic extends BaseGetXController { if (EasyLoading.isShow) { dismissEasyLoading(); // 错误时关闭loading } - showToast('获取锁设置失败 (错误码: $status)'.tr); + // showToast('获取锁设置失败 (错误码: $status)'.tr); state.sureBtnState.value = 0; // 确保重置状态 break; } @@ -585,43 +582,16 @@ class ConfiguringWifiLogic extends BaseGetXController { {required int uploadType, required int recordType, required List records}) async { - try { - final LoginEntity entity = await ApiRepository.to.lockDataUpload( - lockId: state.lockBasicInfo.value.lockId ?? -1, - uploadType: uploadType, - recordType: recordType, - records: records, - isUnShowLoading: true); + final LoginEntity entity = await ApiRepository.to.lockDataUpload( + lockId: state.lockBasicInfo.value.lockId ?? -1, + uploadType: uploadType, + recordType: recordType, + records: records, + isUnShowLoading: true); - if (entity.errorCode!.codeIsSuccessful) { - if (EasyLoading.isShow) { - dismissEasyLoading(); - } - showToast('配网成功'.tr, something: () { - state.sureBtnState.value = 0; // 确保重置状态 - if (state.pageName.value == 'lockSet') { - Get.close(2); - } else { - Get.offAllNamed(Routers.starLockMain); - } - - eventBus.fire( - PassCurrentLockInformationEvent(state.lockSetInfoData.value)); - eventBus.fire(SuccessfulDistributionNetwork()); - }); - } else { - if (EasyLoading.isShow) { - dismissEasyLoading(); - } - // showToast('数据上传失败:${entity.errorCode}'.tr); - state.sureBtnState.value = 0; // 确保重置状态 - } - } catch (e) { - if (EasyLoading.isShow) { - dismissEasyLoading(); - } - // showToast('数据上传异常:${e.toString()}'.tr); - state.sureBtnState.value = 0; // 确保重置状态 + if (entity.errorCode!.codeIsSuccessful) { + eventBus + .fire(PassCurrentLockInformationEvent(state.lockSetInfoData.value)); } } } diff --git a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_page.dart b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_page.dart index 51c45a9d..33a38066 100755 --- a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_page.dart +++ b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_page.dart @@ -51,7 +51,7 @@ class _ConfiguringWifiPageState extends State () => SubmitBtn( btnName: state.sureBtnState.value == 1 ? '配置中...'.tr : '确定'.tr, // 当sureBtnState为1时按钮不可用 - isDisabled: state.sureBtnState.value == 1, + isDisabled: state.sureBtnState.value == 0, onClick: state.sureBtnState.value == 1 ? null : () { diff --git a/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_logic.dart b/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_logic.dart index 6ee5bea6..93532323 100755 --- a/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_logic.dart +++ b/lib/main/lockDetail/lockSet/configuringWifi/wifiList/wifiList_logic.dart @@ -37,14 +37,14 @@ class WifiListLogic extends BaseGetXController { }, onError: (error) { // 处理CRC校验失败等错误 AppLog.log('WiFi列表获取过程中发生错误: $error'); - + // 取消loading状态,显示错误提示 dismissEasyLoading(); cancelBlueConnetctToastTimer(); - + // 重置按钮状态,允许重新扫描 state.sureBtnState.value = 0; - + // 如果是CRC校验失败,显示特定提示 if (error.toString().contains('CRC')) { showToast('数据校验失败,请重新扫描'.tr); @@ -91,29 +91,29 @@ class WifiListLogic extends BaseGetXController { // 把得到的数据按33位分割成数组然后处理 final List> getList = splitList(reply.data, 33); final List> uploadList = >[]; - + for (int i = 0; i < getList.length; i++) { final List indexList = getList[i]; final Map indexMap = {}; final List wifiName = indexList.sublist(0, 32); final String wifiNameStr = utf8String(wifiName).trim(); - + // 过滤掉空的WiFi名称 if (wifiNameStr.isEmpty) { continue; } - + indexMap['wifiName'] = wifiNameStr; indexMap['rssi'] = (indexList.last - 255).toString(); uploadList.add(indexMap); } - + // 按信号强度排序WiFi列表 (从强到弱) - uploadList.sort((a, b) => - int.parse(b['rssi']!).compareTo(int.parse(a['rssi']!))); - + uploadList.sort( + (a, b) => int.parse(b['rssi']!).compareTo(int.parse(a['rssi']!))); + state.wifiNameDataList.value = uploadList; - + if (uploadList.isEmpty) { showToast('未检测到可用的WiFi网络'.tr); } @@ -144,7 +144,7 @@ class WifiListLogic extends BaseGetXController { showBlueConnetctToastTimer(action: () { state.sureBtnState.value = 0; }); - + BlueManage().blueSendData(BlueManage().connectDeviceName, (BluetoothConnectionState connectionState) async { if (connectionState == BluetoothConnectionState.connected) { @@ -172,6 +172,7 @@ class WifiListLogic extends BaseGetXController { super.onReady(); _initReplySubscription(); await senderGetWifiListWifiAction(); + dismissEasyLoading(); } @override From 37f4e6d78eb446efbf66a951507480400d4a439f Mon Sep 17 00:00:00 2001 From: liyi Date: Mon, 28 Apr 2025 09:20:52 +0800 Subject: [PATCH 045/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E5=9B=9E?= =?UTF-8?q?=E5=88=B0=E5=88=97=E8=A1=A8=E5=81=B6=E5=B0=94=E5=87=BA=E7=8E=B0?= =?UTF-8?q?=E7=9B=B8=E5=90=8C=E9=94=81=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/main/lockMian/lockList/lockList_logic.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/main/lockMian/lockList/lockList_logic.dart b/lib/main/lockMian/lockList/lockList_logic.dart index dab00451..4ecd4e62 100755 --- a/lib/main/lockMian/lockList/lockList_logic.dart +++ b/lib/main/lockMian/lockList/lockList_logic.dart @@ -60,9 +60,9 @@ class LockListLogic extends BaseGetXController { //设置数据 void setLockListInfoGroupEntity(LockListInfoGroupEntity entity) { this.entity = entity; - if (entity.pageNo == 1) { + // if (entity.pageNo == 1) { _groupDataList = []; - } + // } _groupDataList.addAll(entity.groupList!); update(); } From bf8d0511c1564efae7abbf20432c869cd8509650 Mon Sep 17 00:00:00 2001 From: liyi Date: Mon, 28 Apr 2025 09:21:11 +0800 Subject: [PATCH 046/151] =?UTF-8?q?fix:=E7=A7=BB=E9=99=A4h264=E3=80=81mjpe?= =?UTF-8?q?g=E5=88=87=E6=8D=A2debug=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lockDetail/lockDetail_logic.dart | 8 --- .../lockDetail/lockDetail_page.dart | 54 ------------------- .../lockDetail/lockDetail_state.dart | 3 -- 3 files changed, 65 deletions(-) diff --git a/lib/main/lockDetail/lockDetail/lockDetail_logic.dart b/lib/main/lockDetail/lockDetail/lockDetail_logic.dart index 72683016..9c11e66b 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_logic.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_logic.dart @@ -798,14 +798,6 @@ class LockDetailLogic extends BaseGetXController { void onInit() { super.onInit(); - // 初始化开关状态为当前对讲视频模式 - final currentTalkExpect = StartChartManage().getDefaultTalkExpect(); - if (currentTalkExpect.videoType.contains(VideoTypeE.H264)) { - state.useH264Mode.value = true; - } else if (currentTalkExpect.videoType.contains(VideoTypeE.IMAGE)) { - state.useH264Mode.value = false; - } - state.LockSetChangeSetRefreshLockDetailWithTypeSubscription = eventBus .on() .listen((LockSetChangeSetRefreshLockDetailWithType event) { diff --git a/lib/main/lockDetail/lockDetail/lockDetail_page.dart b/lib/main/lockDetail/lockDetail/lockDetail_page.dart index 93ea8f2f..ec09e9a3 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_page.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_page.dart @@ -507,60 +507,6 @@ class _LockDetailPageState extends State Widget skWidget() { return ListView( children: [ - // Container( - // padding: EdgeInsets.symmetric(vertical: 15, horizontal: 20), - // margin: EdgeInsets.only(top: 10, bottom: 10), - // decoration: BoxDecoration( - // color: Colors.white, - // borderRadius: BorderRadius.circular(10), - // boxShadow: [ - // BoxShadow( - // color: Colors.black.withOpacity(0.05), - // blurRadius: 5, - // offset: Offset(0, 2), - // ), - // ], - // ), - // child: Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // Text('对讲视频模式'.tr, - // style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), - // Row( - // children: [ - // Text('mjpeg', - // style: TextStyle( - // fontSize: 14, - // color: !state.useH264Mode.value - // ? AppColors.mainColor - // : Colors.grey)), - // Obx(() => Switch( - // value: state.useH264Mode.value, - // activeColor: AppColors.mainColor, - // onChanged: (value) { - // state.useH264Mode.value = value; - // if (value) { - // // 使用H264模式 - // StartChartManage() - // .sendH264VideoAndG711AudioTalkExpectData(); - // } else { - // // 使用Image模式 - // StartChartManage() - // .sendImageVideoAndG711AudioTalkExpectData(); - // } - // }, - // )), - // Text('H264'.tr, - // style: TextStyle( - // fontSize: 14, - // color: state.useH264Mode.value - // ? AppColors.mainColor - // : Colors.grey)), - // ], - // ), - // ], - // ), - // ), Visibility( visible: (state.keyInfos.value.keyType == XSConstantMacro.keyTypeTime || diff --git a/lib/main/lockDetail/lockDetail/lockDetail_state.dart b/lib/main/lockDetail/lockDetail/lockDetail_state.dart index a949b693..44efb77d 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_state.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_state.dart @@ -58,9 +58,6 @@ class LockDetailState { int logCountPage = 10; // 蓝牙记录一页多少个 RxInt nextAuthTime = 0.obs; // 下次认证时间 - // 视频编码模式选择开关状态 - RxBool useH264Mode = true.obs; // true表示使用H264模式,false表示使用Image模式 - // LockDetailState() { // Map map = Get.arguments; // lockCount = map["lockCount"]; From 1543de142fa87ff73625bd57f9191ddab509f165 Mon Sep 17 00:00:00 2001 From: liyi Date: Mon, 28 Apr 2025 09:21:50 +0800 Subject: [PATCH 047/151] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0=E6=8F=92?= =?UTF-8?q?=E4=BB=B6=E6=92=AD=E6=94=BE=E9=A1=B5=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/appRouters.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/appRouters.dart b/lib/appRouters.dart index 83b41650..4990d37f 100755 --- a/lib/appRouters.dart +++ b/lib/appRouters.dart @@ -1185,7 +1185,7 @@ abstract class AppRouters { page: () => const DoubleLockLinkPage()), GetPage( name: Routers.starChartTalkView, page: () => const TalkViewPage()), - // GetPage(name: Routers.h264WebView, page: () => TalkViewNativeDecodePage()), - GetPage(name: Routers.h264WebView, page: () => H264WebView()), + // GetPage(name: Routers.h264WebView, page: () => TalkViewNativeDecodePage()), // 插件播放页面 + GetPage(name: Routers.h264WebView, page: () => H264WebView()), // webview播放页面 ]; } From 84431d17c4ded8bc0ad86debe12703467bcd322c Mon Sep 17 00:00:00 2001 From: liyi Date: Mon, 28 Apr 2025 10:59:48 +0800 Subject: [PATCH 048/151] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E6=97=8B=E8=BD=AC=E8=A7=92=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/html/h264.html | 4 ++-- .../handle/impl/udp_talk_expect_handler.dart | 1 + lib/talk/starChart/star_chart_manage.dart | 2 ++ lib/talk/starChart/views/talkView/talk_view_page.dart | 4 +++- lib/talk/starChart/webView/h264_web_view.dart | 11 +++++++++-- 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/assets/html/h264.html b/assets/html/h264.html index 0995b612..60ec7039 100644 --- a/assets/html/h264.html +++ b/assets/html/h264.html @@ -29,8 +29,8 @@ #player { object-fit: cover; - height: 56vh; - transform: rotate(-90deg); + width: 100vw; + height: 100vh; } diff --git a/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart index d1c83e77..3d8e078d 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart @@ -41,6 +41,7 @@ class UdpTalkExpectHandler extends ScpMessageBaseHandle // 停止发送对讲请求 startChartManage.stopCallRequestMessageTimer(); // talkViewState.rotateAngle.value = talkExpectResp.rotate ?? 0; + startChartManage.rotateAngle = talkExpectResp.rotate; // 收到预期数据的应答后,代表建立了连接,启动通话保持的监听 // 启动通话保持监听定时器(用来判断如果x秒内没有收到通话保持则执行的操作); talkePingOverTimeTimerManager.start(); diff --git a/lib/talk/starChart/star_chart_manage.dart b/lib/talk/starChart/star_chart_manage.dart index 1fb79272..5018bac5 100644 --- a/lib/talk/starChart/star_chart_manage.dart +++ b/lib/talk/starChart/star_chart_manage.dart @@ -112,6 +112,8 @@ class StartChartManage { RbcuConfirm? rbcuConfirm; final int _maxPayloadSize = 8 * 1024; // 分包大小 + int rotateAngle = 0; // 视频旋转角度 + // 默认通话的期望数据格式 TalkExpectReq _defaultTalkExpect = TalkConstant.H264Expect; diff --git a/lib/talk/starChart/views/talkView/talk_view_page.dart b/lib/talk/starChart/views/talkView/talk_view_page.dart index 38b22343..38dd4896 100644 --- a/lib/talk/starChart/views/talkView/talk_view_page.dart +++ b/lib/talk/starChart/views/talkView/talk_view_page.dart @@ -12,6 +12,7 @@ import 'package:star_lock/talk/call/callTalk.dart'; import 'package:star_lock/talk/starChart/constant/talk_status.dart'; import 'package:star_lock/talk/starChart/handle/impl/debug_Info_model.dart'; import 'package:star_lock/talk/starChart/handle/impl/udp_talk_data_handler.dart'; +import 'package:star_lock/talk/starChart/star_chart_manage.dart'; import 'package:star_lock/talk/starChart/views/talkView/talk_view_logic.dart'; import 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart'; @@ -30,6 +31,7 @@ class _TalkViewPageState extends State final TalkViewLogic logic = Get.put(TalkViewLogic()); final TalkViewState state = Get.find().state; late Stream _latencyStream; + final startChartManage = StartChartManage(); @override void initState() { @@ -132,7 +134,7 @@ class _TalkViewPageState extends State key: state.globalKey, child: SizedBox.expand( child: RotatedBox( - quarterTurns: -1, + quarterTurns: startChartManage.remotePort ~/ 90, child: Obx( () => state.currentImage.value != null ? RawImage( diff --git a/lib/talk/starChart/webView/h264_web_view.dart b/lib/talk/starChart/webView/h264_web_view.dart index aa18aa3c..03f97609 100644 --- a/lib/talk/starChart/webView/h264_web_view.dart +++ b/lib/talk/starChart/webView/h264_web_view.dart @@ -26,6 +26,7 @@ class _H264WebViewState extends State with TickerProviderStateMixin { final H264WebViewLogic logic = Get.put(H264WebViewLogic()); final H264WebViewState state = Get.find().state; + final startChartManage = StartChartManage(); @override void initState() { @@ -72,8 +73,13 @@ class _H264WebViewState extends State height: screenHeight, fit: BoxFit.cover, ) - : WebViewWidget( - controller: state.webViewController, + : SizedBox.expand( + child: RotatedBox( + quarterTurns: startChartManage.remotePort ~/ 90, + child: WebViewWidget( + controller: state.webViewController, + ), + ), ); }), Obx( @@ -412,6 +418,7 @@ class _H264WebViewState extends State ), ); } + @override void dispose() { state.animationController.dispose(); From 419912c590c3d2e824a5ebe8770c278ef636c7a6 Mon Sep 17 00:00:00 2001 From: liyi Date: Mon, 28 Apr 2025 11:47:26 +0800 Subject: [PATCH 049/151] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E6=97=8B=E8=BD=AC=E8=A7=92=E5=BA=A6=EF=BC=8C=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E7=9B=91=E6=8E=A7=E6=97=B6=E8=8E=B7=E5=8F=96=E9=94=81?= =?UTF-8?q?peerId=E7=9A=84=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lockDetail/lockDetail_logic.dart | 61 +++++++++---------- .../handle/impl/udp_talk_expect_handler.dart | 2 + .../handle/impl/udp_talk_request_handler.dart | 12 ++-- lib/talk/starChart/star_chart_manage.dart | 42 +++++++++---- .../native/talk_view_native_decode_logic.dart | 28 ++++++--- .../views/talkView/talk_view_page.dart | 2 +- lib/talk/starChart/webView/h264_web_view.dart | 2 +- 7 files changed, 91 insertions(+), 58 deletions(-) diff --git a/lib/main/lockDetail/lockDetail/lockDetail_logic.dart b/lib/main/lockDetail/lockDetail/lockDetail_logic.dart index 9c11e66b..7c086ba5 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_logic.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_logic.dart @@ -743,43 +743,40 @@ class LockDetailLogic extends BaseGetXController { eventBus.fire(RefreshLockDetailInfoDataEvent()); } - /// 请求设备网络信息并设置 - void requestDeviceNetworkInfo() async { - final DeviceNetwork deviceNetworkInfo = - await ApiRepository.to.getDeviceNetwork( - deviceType: 2, - deviceMac: state.keyInfos.value.mac!, - ); - if (deviceNetworkInfo.data?.wifiName == null || - deviceNetworkInfo.data?.wifiName == '') { - return; - } else { - final peerId = deviceNetworkInfo?.data?.peerId; - if (peerId == null || peerId.isEmpty || peerId == '') { - throw Exception('设备peerId为空'); - } - // 设置锁的peerID - StartChartManage().lockNetworkInfo = - deviceNetworkInfo.data ?? DeviceNetworkInfo(); - StartChartManage().lockPeerId = peerId; - } - } + // /// 请求设备网络信息并设置 + // void requestDeviceNetworkInfo() async { + // final DeviceNetwork deviceNetworkInfo = + // await ApiRepository.to.getDeviceNetwork( + // deviceType: 2, + // deviceMac: state.keyInfos.value.mac!, + // ); + // if (deviceNetworkInfo.data?.peerId == null || + // deviceNetworkInfo.data?.peerId == '') { + // return; + // } + // final peerId = deviceNetworkInfo!.data!.peerId; + // // 设置锁的peerID + // StartChartManage().lockNetworkInfo = + // deviceNetworkInfo.data ?? DeviceNetworkInfo(); + // StartChartManage().lockPeerId = peerId!; + // } /// 发送监控消息 void sendMonitorMessage() { final catEyeConfig = state.keyInfos.value.lockSetting?.catEyeConfig ?? []; + final network = state.keyInfos.value.network; if (catEyeConfig.isNotEmpty && catEyeConfig.length > 0 && catEyeConfig[0].catEyeMode != 0) { - if ((StartChartManage().lockNetworkInfo.wifiName == null || - StartChartManage().lockNetworkInfo.wifiName == '') ) { + if (network == null || network?.peerId == null || network?.peerId == '') { showToast('设备未配网'.tr); return; } + // 重置丢包率监控 PacketLossStatistics().reset(); // 发送监控id - StartChartManage().startCallRequestMessageTimer( - ToPeerId: StartChartManage().lockNetworkInfo.peerId ?? ''); + StartChartManage() + .startCallRequestMessageTimer(ToPeerId: network!.peerId ?? ''); } else { showToast('猫眼设置为省电模式时无法进行监控,请在猫眼设置中切换为其他模式'.tr); } @@ -791,7 +788,7 @@ class LockDetailLogic extends BaseGetXController { getServerDatetime(); await PermissionDialog.request(Permission.location); await PermissionDialog.requestBluetooth(); - requestDeviceNetworkInfo(); + // requestDeviceNetworkInfo(); } @override @@ -844,11 +841,11 @@ class LockDetailLogic extends BaseGetXController { } }); - state.SuccessfulDistributionNetworkEvent = eventBus - .on() - .listen((SuccessfulDistributionNetwork event) { - // 配网成功获取一下配网信息 - requestDeviceNetworkInfo(); - }); + // state.SuccessfulDistributionNetworkEvent = eventBus + // .on() + // .listen((SuccessfulDistributionNetwork event) { + // // 配网成功获取一下配网信息 + // requestDeviceNetworkInfo(); + // }); } } diff --git a/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart index 3d8e078d..e9f3e598 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart @@ -4,6 +4,7 @@ import 'dart:typed_data'; import 'package:flutter/services.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:get/get.dart'; +import 'package:star_lock/app_settings/app_settings.dart'; import 'package:star_lock/talk/starChart/constant/message_type_constant.dart'; import 'package:star_lock/talk/starChart/constant/talk_status.dart'; import 'package:star_lock/talk/starChart/entity/scp_message.dart'; @@ -42,6 +43,7 @@ class UdpTalkExpectHandler extends ScpMessageBaseHandle startChartManage.stopCallRequestMessageTimer(); // talkViewState.rotateAngle.value = talkExpectResp.rotate ?? 0; startChartManage.rotateAngle = talkExpectResp.rotate; + AppLog.log('视频画面需要旋转:${talkExpectResp.rotate}'); // 收到预期数据的应答后,代表建立了连接,启动通话保持的监听 // 启动通话保持监听定时器(用来判断如果x秒内没有收到通话保持则执行的操作); talkePingOverTimeTimerManager.start(); diff --git a/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart index 91fdcc83..3a26c1fa 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart @@ -180,15 +180,15 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle if (isH264) { // 锁支持H264,发送H264视频和G711音频期望 startChartManage.sendOnlyH264VideoTalkExpectData(); - print('锁支持H264,发送H264视频格式期望数据'); + print('app收到的对讲请求后,发送的预期数据=========锁支持H264,发送H264视频格式期望数据'); } else if (isMJpeg) { // 锁只支持MJPEG,发送图像视频和G711音频期望 startChartManage.sendOnlyImageVideoTalkExpectData(); - print('锁不支持H264,支持MJPEG,发送MJPEG视频格式期望数据'); + print('app收到的对讲请求后,发送的预期数据=========锁不支持H264,支持MJPEG,发送MJPEG视频格式期望数据'); } else { // 默认使用图像视频 startChartManage.sendOnlyImageVideoTalkExpectData(); - print('锁不支持H264和MJPEG,默认发送图像视频格式期望数据'); + print('app收到的对讲请求后,发送的预期数据=========锁不支持H264和MJPEG,默认发送图像视频格式期望数据'); } } @@ -203,15 +203,15 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle if (isH264) { // 锁支持H264,发送H264视频和G711音频期望 startChartManage.sendH264VideoAndG711AudioTalkExpectData(); - print('锁支持H264,发送H264视频格式期望数据'); + print('app主动发请求,收到回复后发送的预期数据=======锁支持H264,发送H264视频格式期望数据'); } else if (isMJpeg) { // 锁只支持MJPEG,发送图像视频和G711音频期望 startChartManage.sendImageVideoAndG711AudioTalkExpectData(); - print('锁不支持H264,支持MJPEG,发送MJPEG视频格式期望数据'); + print('app主动发请求,收到回复后发送的预期数据=======锁不支持H264,支持MJPEG,发送MJPEG视频格式期望数据'); } else { // 默认使用图像视频 startChartManage.sendImageVideoAndG711AudioTalkExpectData(); - print('锁不支持H264和MJPEG,默认发送图像视频格式期望数据'); + print('app主动发请求,收到回复后发送的预期数据=======锁不支持H264和MJPEG,默认发送图像视频格式期望数据'); } } } diff --git a/lib/talk/starChart/star_chart_manage.dart b/lib/talk/starChart/star_chart_manage.dart index 5018bac5..e5eddb59 100644 --- a/lib/talk/starChart/star_chart_manage.dart +++ b/lib/talk/starChart/star_chart_manage.dart @@ -45,6 +45,7 @@ import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart'; import 'package:star_lock/talk/starChart/proto/talk_expect.pbserver.dart'; import 'package:star_lock/talk/starChart/status/star_chart_talk_status.dart'; import 'package:star_lock/tools/baseGetXController.dart'; +import 'package:star_lock/tools/commonDataManage.dart'; import 'package:star_lock/tools/deviceInfo_utils.dart'; import 'package:star_lock/tools/storage.dart'; import 'package:uuid/uuid.dart'; @@ -407,17 +408,36 @@ class StartChartManage { /// 启动持续发送对讲请求 void startCallRequestMessageTimer({required String ToPeerId}) async { // 如果已经处于等待接听状态就不发送 - if (talkStatus.status != TalkStatus.proactivelyCallWaitingAnswer) { - // 如果是h264则跳转至webview - if (_defaultTalkExpect.videoType.contains(VideoTypeE.H264)) { - Get.toNamed( - Routers.h264WebView, - ); - } else { - Get.toNamed( - Routers.starChartTalkView, - ); - } + // if (talkStatus.status != TalkStatus.proactivelyCallWaitingAnswer) { + // // 如果是h264则跳转至webview + // if (_defaultTalkExpect.videoType.contains(VideoTypeE.H264)) { + // Get.toNamed( + // Routers.h264WebView, + // ); + // } else { + // Get.toNamed( + // Routers.starChartTalkView, + // ); + // } + // } + final LockListInfoItemEntity currentKeyInfo = + CommonDataManage().currentKeyInfo; + final isH264 = currentKeyInfo.lockFeature?.isH264 == 1; + final isMJpeg = currentKeyInfo.lockFeature?.isMJpeg == 1; + + // 优先使用H264,其次是MJPEG + if (isH264) { + Get.toNamed( + Routers.h264WebView, + ); + } else if (isMJpeg) { + Get.toNamed( + Routers.starChartTalkView, + ); + } else { + Get.toNamed( + Routers.starChartTalkView, + ); } // 启动定时器持续发送对讲请求 talkRequestTimer ??= Timer.periodic( diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart index cf76414e..2fad25a7 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart @@ -59,6 +59,9 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { // 添加一个集合来跟踪已成功解码的I帧序号 final Set _decodedIFrames = {}; + // 定义一个变量来保存上一帧的时间戳 + int? _previousFrameTimestamp; + // 初始化视频解码器 Future _initVideoDecoder() async { try { @@ -227,16 +230,27 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { _playAudioFrames(); break; case TalkData_ContentTypeE.H264: + // 获取当前时间戳 + final currentTimestamp = DateTime.now().millisecondsSinceEpoch; + + // 如果存在上一帧时间戳,则计算时间间隔 + if (_previousFrameTimestamp != null) { + final timeDifference = currentTimestamp - _previousFrameTimestamp!; + AppLog.log('当前帧与上一帧的时间间隔: $timeDifference 毫秒'); + } + // 更新上一帧时间戳为当前帧的时间戳 + _previousFrameTimestamp = currentTimestamp; + // 添加到视频帧缓冲区,而不是直接处理 // _processH264Frame(talkData, talkDataH264Frame!); // 直接处理H264视频帧 - _processH264Frame(talkData, talkDataH264Frame!); - - // 记录关键调试信息 - if (talkDataH264Frame!.frameType == TalkDataH264Frame_FrameTypeE.I) { - AppLog.log( - '帧序号${talkDataH264Frame.frameSeq};帧类型:${talkDataH264Frame.frameType.toString()};时间戳:${DateTime.now().millisecondsSinceEpoch}'); - } + // _processH264Frame(talkData, talkDataH264Frame!); + // + // // 记录关键调试信息 + // if (talkDataH264Frame!.frameType == TalkDataH264Frame_FrameTypeE.I) { + // AppLog.log( + // '帧序号${talkDataH264Frame.frameSeq};帧类型:${talkDataH264Frame.frameType.toString()};时间戳:${DateTime.now().millisecondsSinceEpoch}'); + // } break; } }); diff --git a/lib/talk/starChart/views/talkView/talk_view_page.dart b/lib/talk/starChart/views/talkView/talk_view_page.dart index 38dd4896..f9159cee 100644 --- a/lib/talk/starChart/views/talkView/talk_view_page.dart +++ b/lib/talk/starChart/views/talkView/talk_view_page.dart @@ -134,7 +134,7 @@ class _TalkViewPageState extends State key: state.globalKey, child: SizedBox.expand( child: RotatedBox( - quarterTurns: startChartManage.remotePort ~/ 90, + quarterTurns: startChartManage.rotateAngle ~/ 90, child: Obx( () => state.currentImage.value != null ? RawImage( diff --git a/lib/talk/starChart/webView/h264_web_view.dart b/lib/talk/starChart/webView/h264_web_view.dart index 03f97609..f0f11885 100644 --- a/lib/talk/starChart/webView/h264_web_view.dart +++ b/lib/talk/starChart/webView/h264_web_view.dart @@ -75,7 +75,7 @@ class _H264WebViewState extends State ) : SizedBox.expand( child: RotatedBox( - quarterTurns: startChartManage.remotePort ~/ 90, + quarterTurns: startChartManage.rotateAngle ~/ 90, child: WebViewWidget( controller: state.webViewController, ), From e806987fa0ae4f3b58b6699adfaf279330217d57 Mon Sep 17 00:00:00 2001 From: liyi Date: Wed, 30 Apr 2025 10:25:19 +0800 Subject: [PATCH 050/151] =?UTF-8?q?fix:=E5=AE=8C=E6=88=90720P=2020?= =?UTF-8?q?=E5=B8=A7=E6=B8=B2=E6=9F=93=E9=9C=80=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../starChart/proto/talk_expect.pbenum.dart | 2 + .../native/talk_view_native_decode_logic.dart | 921 +++++++++++++----- .../native/talk_view_native_decode_page.dart | 194 ++-- .../native/talk_view_native_decode_state.dart | 15 + 4 files changed, 774 insertions(+), 358 deletions(-) diff --git a/lib/talk/starChart/proto/talk_expect.pbenum.dart b/lib/talk/starChart/proto/talk_expect.pbenum.dart index d6b34250..10306109 100644 --- a/lib/talk/starChart/proto/talk_expect.pbenum.dart +++ b/lib/talk/starChart/proto/talk_expect.pbenum.dart @@ -19,12 +19,14 @@ class VideoTypeE extends $pb.ProtobufEnum { 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 VideoTypeE H264_720P = VideoTypeE._(4, _omitEnumNames ? '' : 'H264_720P'); static const $core.List values = [ NONE_V, H264, IMAGE, VP8, + H264_720P, ]; static final $core.Map<$core.int, VideoTypeE> _byValue = $pb.ProtobufEnum.initByValue(values); diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart index 2fad25a7..8129f204 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'dart:ui' as ui; import 'dart:math'; // Import the math package to use sqrt +import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; @@ -62,41 +63,76 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { // 定义一个变量来保存上一帧的时间戳 int? _previousFrameTimestamp; + int _flutterToNativeFrameCount = 0; + int _lastFlutterToNativePrintTime = 0; + int _networkFrameCount = 0; + int _lastNetworkPrintTime = 0; + Timer? _frameRefreshTimer; + + bool _isFrameAvailable = true; + int _renderedFrameCount = 0; + int _lastRenderedFrameTime = 0; + + // 写入前的缓存队列(I帧前) + final List> _preIFrameCache = []; + bool _hasWrittenFirstIFrame = false; + + bool _isStartNative = false; + + // 新增:SPS/PPS状态追踪变量 + bool hasSps = false; + bool hasPps = false; + + // 新增:SPS/PPS缓存 + List? spsCache; + List? ppsCache; + + void _setupFrameRefresh() { + // 设置帧刷新定时器,16ms对应约60fps + _frameRefreshTimer = + Timer.periodic(const Duration(milliseconds: 16), (timer) { + if (_isFrameAvailable) { + _isFrameAvailable = false; + _renderedFrameCount++; + + // 每秒统计一次帧率 + int now = DateTime.now().millisecondsSinceEpoch; + if (now - _lastRenderedFrameTime > 1000) { + print('[Flutter] 每秒渲染帧数: $_renderedFrameCount'); + _renderedFrameCount = 0; + _lastRenderedFrameTime = now; + } + + // 请求Flutter重建widget + WidgetsBinding.instance.scheduleFrame(); + } + }); + } + + void onFrameAvailable() { + _isFrameAvailable = true; + } + // 初始化视频解码器 Future _initVideoDecoder() async { try { // 创建解码器配置 final config = VideoDecoderConfig( - width: 864, + width: 1280, // 实际视频宽度 - height: 480, - frameRate: 25, - // 明确设置帧率 - // 增大缓冲区大小 - codecType: CodecType.h264, - // 编解码类型 - isDebug: true, + height: 720, + codecType: 'h264', ); - // 初始化解码器并获取textureId final textureId = await VideoDecodePlugin.initDecoder(config); - if (textureId != null) { state.textureId.value = textureId; AppLog.log('视频解码器初始化成功:textureId=$textureId'); - - // 设置帧回调 - VideoDecodePlugin.setFrameCallback(_onFrameAvailable); - - // 设置状态回调 - VideoDecodePlugin.setStateCallbackForTexture( - textureId, _onDecoderStateChanged); - - // 启动FPS监测 - startFpsMonitoring(); + // 启动帧处理定时器 } else { AppLog.log('视频解码器初始化失败'); } + _startFrameProcessTimer(); } catch (e) { AppLog.log('初始化视频解码器错误: $e'); // 如果初始化失败,延迟后重试 @@ -108,66 +144,6 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { } } - // 添加帧可用回调 - void _onFrameAvailable(int textureId) {} - - // 解码器状态变化回调 - void _onDecoderStateChanged( - int textureId, DecoderState decoderState, Map stats) { - String stateText; - switch (decoderState) { - case DecoderState.initializing: - state.isLoading.value = true; - stateText = "初始化中"; - break; - case DecoderState.ready: - stateText = "准备就绪"; - break; - case DecoderState.rendering: - stateText = "渲染中"; - state.isLoading.value = false; - break; - case DecoderState.error: - stateText = "出错"; - // 获取错误信息 - final errorMessage = stats['errorMessage'] as String?; - if (errorMessage != null) { - AppLog.log("解码器错误: $errorMessage"); - } - break; - case DecoderState.released: - stateText = "已释放"; - break; - default: - stateText = "未知状态"; - } - - // 更新统计信息 - if (stats.isNotEmpty) { - // 获取丢包率统计信息 - final PacketLossInfo packetLossInfo = - PacketLossStatistics().getStatistics(); - - // 更新FPS - // state.decoderFps.value = (stats['fps'] as num?)?.toDouble() ?? 0.0; - // 更新解码器详细信息 - state.renderedFrameCount.value = (stats['renderedFrames'] as int?) ?? 0; - state.totalFrames.value = (stats['totalFrames'] as int?) ?? 0; - state.droppedFrames.value = (stats['droppedFrames'] as int?) ?? 0; - state.hasSentIDR.value = (stats['hasSentIDR'] as bool?) ?? false; - state.hasSentSPS.value = (stats['hasSentSPS'] as bool?) ?? false; - state.hasSentPPS.value = (stats['hasSentPPS'] as bool?) ?? false; - state.keyFrameInterval.value = (stats['keyFrameInterval'] as int?) ?? 0; - state.decodingJitterMs.value = (stats['decodingJitterMs'] as int?) ?? 0; - - // 更新状态数据 - state.messageLossRate.value = packetLossInfo.messageLossRate; - state.packetLossRate.value = packetLossInfo.packetLossRate; - state.lastPacketStatsUpdateTime.value = - DateTime.now().millisecondsSinceEpoch; - } - } - /// 初始化音频播放器 void _initFlutterPcmSound() { const int sampleRate = 8000; @@ -190,13 +166,149 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { // 拒绝 StartChartManage().startTalkRejectMessageTimer(); } - if (state.textureId.value != null) { - VideoDecodePlugin.releaseDecoderForTexture(state.textureId.value!); - } - VideoDecodePlugin.releaseAllDecoders(); + VideoDecodePlugin.releaseDecoder(); Get.back(); } + /// 添加H264帧到缓冲区 + void _addFrameToBuffer( + List frameData, + TalkDataH264Frame_FrameTypeE frameType, + int pts, + int frameSeq, + int frameSeqI, + ) { + _networkFrameCount++; + int now = DateTime.now().millisecondsSinceEpoch; + if (now - _lastNetworkPrintTime > 1000) { + AppLog.log('[Flutter] 每秒收到网络H264帧数: ' + _networkFrameCount.toString()); + state.networkH264Fps.value = _networkFrameCount; + _networkFrameCount = 0; + _lastNetworkPrintTime = now; + } + + // 创建包含帧数据和类型的Map + final Map frameMap = { + 'frameData': frameData, + 'frameType': frameType, + 'frameSeq': frameSeq, + 'frameSeqI': frameSeqI, + 'pts': pts, + }; + + // 将帧添加到缓冲区 + state.h264FrameBuffer.add(frameMap); + + // 如果缓冲区超出最大大小,移除最早的帧 + while (state.h264FrameBuffer.length > state.maxFrameBufferSize) { + state.h264FrameBuffer.removeAt(0); + } + + _flutterToNativeFrameCount++; + if (now - _lastFlutterToNativePrintTime > 1000) { + AppLog.log( + '[Flutter] 每秒送入Native帧数: ' + _flutterToNativeFrameCount.toString()); + state.nativeSendFps.value = _flutterToNativeFrameCount; + _flutterToNativeFrameCount = 0; + _lastFlutterToNativePrintTime = now; + } + } + + /// 启动帧处理定时器 + void _startFrameProcessTimer() { + // 取消已有定时器 + state.frameProcessTimer?.cancel(); + + // 计算定时器间隔,确保以目标帧率处理帧 + final int intervalMs = (1000 / state.targetFps).round(); + + // 创建新定时器 + state.frameProcessTimer = + Timer.periodic(Duration(milliseconds: intervalMs), (timer) { + if (state.isLoading.isTrue) { + state.isLoading.value = false; + } + _processNextFrameFromBuffer(); + }); + + AppLog.log('启动帧处理定时器,目标帧率: ${state.targetFps}fps,间隔: ${intervalMs}ms'); + } + + /// 从缓冲区处理下一帧 + void _processNextFrameFromBuffer() async { + // 避免重复处理 + if (state.isProcessingFrame) { + return; + } + + // 如果缓冲区为空,跳过 + if (state.h264FrameBuffer.isEmpty) { + return; + } + + // 设置正在处理标志 + state.isProcessingFrame = true; + + try { + // 取出最早的帧 + final Map frameMap = state.h264FrameBuffer.removeAt(0); + final List frameData = frameMap['frameData']; + final TalkDataH264Frame_FrameTypeE frameType = frameMap['frameType']; + final int frameSeq = frameMap['frameSeq']; + final int frameSeqI = frameMap['frameSeqI']; + int pts = DateTime.now().millisecondsSinceEpoch; + + if (frameType == TalkDataH264Frame_FrameTypeE.P) { + // 以frameSeqI为I帧序号标识 + if (!(_decodedIFrames.contains(frameSeqI))) { + AppLog.log('丢弃P帧:未收到对应I帧,frameSeqI=${frameSeqI}'); + return; + } + } else if (frameType == TalkDataH264Frame_FrameTypeE.I) { + // 记录已解码I帧序号 + _decodedIFrames.add(frameSeq); + } + // 实时写入h264文件 + _appendH264FrameToFile(frameData, frameType); + + final timestamp = DateTime.now().microsecondsSinceEpoch; + VideoDecodePlugin.decodeFrame( + frameData: Uint8List.fromList(frameData), + frameType: frameType == TalkDataH264Frame_FrameTypeE.I ? 1 : 0, + frameSeq: frameSeq, + timestamp: timestamp, + refIFrameSeq: frameSeqI, + ); + + // 判断P帧对应I帧是否已解码,未解码则丢弃P帧 + if (frameType == TalkDataH264Frame_FrameTypeE.P) { + // 以frameSeqI为I帧序号标识 + if (!(_decodedIFrames.contains(frameSeqI))) { + AppLog.log('丢弃P帧:未收到对应I帧,frameSeqI=${frameSeqI}'); + } + } else if (frameType == TalkDataH264Frame_FrameTypeE.I) { + // 记录已解码I帧序号 + _decodedIFrames.add(frameSeq); + } + // 实时写入h264文件 + _appendH264FrameToFile(frameData, frameType); + } catch (e) { + AppLog.log('处理缓冲帧失败: $e'); + } finally { + // 重置处理标志 + state.isProcessingFrame = false; + } + } + + /// 停止帧处理定时器 + void _stopFrameProcessTimer() { + state.frameProcessTimer?.cancel(); + state.frameProcessTimer = null; + state.h264FrameBuffer.clear(); + state.isProcessingFrame = false; + // AppLog.log('停止帧处理定时器'); + } + // 发起接听命令 void initiateAnswerCommand() { StartChartManage().startTalkAcceptTimer(); @@ -230,82 +342,63 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { _playAudioFrames(); break; case TalkData_ContentTypeE.H264: - // 获取当前时间戳 - final currentTimestamp = DateTime.now().millisecondsSinceEpoch; - - // 如果存在上一帧时间戳,则计算时间间隔 - if (_previousFrameTimestamp != null) { - final timeDifference = currentTimestamp - _previousFrameTimestamp!; - AppLog.log('当前帧与上一帧的时间间隔: $timeDifference 毫秒'); - } - // 更新上一帧时间戳为当前帧的时间戳 - _previousFrameTimestamp = currentTimestamp; - - // 添加到视频帧缓冲区,而不是直接处理 - // _processH264Frame(talkData, talkDataH264Frame!); - // 直接处理H264视频帧 - // _processH264Frame(talkData, talkDataH264Frame!); - // - // // 记录关键调试信息 - // if (talkDataH264Frame!.frameType == TalkDataH264Frame_FrameTypeE.I) { - // AppLog.log( - // '帧序号${talkDataH264Frame.frameSeq};帧类型:${talkDataH264Frame.frameType.toString()};时间戳:${DateTime.now().millisecondsSinceEpoch}'); + // if (_isStartNative) { + // if (talkDataH264Frame != null) { + // // 如果是I帧,先分割NALU,找到SPS/PPS并优先放入缓冲区 + // if (talkDataH264Frame.frameType == + // TalkDataH264Frame_FrameTypeE.I) { + // // 清空缓冲区,丢弃I帧前所有未处理帧(只保留SPS/PPS/I帧) + // state.h264FrameBuffer.clear(); + // _extractAndBufferSpsPpsForBuffer( + // talkData.content, + // talkData.durationMs, + // talkDataH264Frame.frameSeq, + // talkDataH264Frame.frameSeqI); + // } + // _addFrameToBuffer( + // talkData.content, + // talkDataH264Frame.frameType, + // talkData.durationMs, + // talkDataH264Frame.frameSeq, + // talkDataH264Frame.frameSeqI); + // } + // } else { + // await VideoDecodePlugin.startNativePlayer( + // VideoDecoderConfig(width: 1280, height: 720, codecType: 'h264'), + // ); + // _isStartNative = true; // } + // 处理H264帧 + if (state.textureId.value != null) { + if (talkDataH264Frame != null) { + if (talkDataH264Frame.frameType == + TalkDataH264Frame_FrameTypeE.I) { + _handleIFrameWithSpsPpsAndIdr( + talkData.content, + talkData.durationMs, + talkDataH264Frame.frameSeq, + talkDataH264Frame.frameSeqI, + ); + return; + } else if (talkDataH264Frame.frameType == + TalkDataH264Frame_FrameTypeE.P) { + _handlePFrame( + talkData.content, + talkData.durationMs, + talkDataH264Frame.frameSeq, + talkDataH264Frame.frameSeqI, + ); + return; + } + } + } else { + AppLog.log('无法处理H264帧:textureId为空'); + } break; } }); } - // 处理H264视频帧 - Future _processH264Frame( - TalkData talkData, TalkDataH264Frame frameInfo) async { - // 检查解码器是否已初始化 - if (state.textureId.value == null) { - // 可以记录日志或尝试初始化解码器 - AppLog.log('解码器尚未初始化,尝试重新初始化...'); - await _initVideoDecoder(); - - // 如果仍未初始化成功,则丢弃此帧 - if (state.textureId.value == null) { - return; - } - } - // 获取P帧对应的I帧序号 - final frameSeqI = frameInfo.frameSeqI; - - // P帧检查:如果依赖的I帧未解码成功,直接丢弃 - if (frameInfo.frameType == TalkDataH264Frame_FrameTypeE.P && - !_decodedIFrames.contains(frameSeqI)) { - AppLog.log('丢弃P帧: 依赖的I帧(${frameSeqI})尚未解码, P帧序号: ${frameInfo.frameSeq}'); - return; - } - - // 从talkData中提取H264帧数据 - final Uint8List frameData = Uint8List.fromList(talkData.content); - - // 确定帧类型 - final FrameType frameType = - frameInfo.frameType == TalkDataH264Frame_FrameTypeE.I - ? FrameType.iFrame - : FrameType.pFrame; - - // 将帧数据交给解码器处理 - try { - final bool result = - await VideoDecodePlugin.decodeFrame(frameData, frameType); - // 如果是I帧且成功解码,将其序号加入已解码I帧集合 - if (frameInfo.frameType == TalkDataH264Frame_FrameTypeE.I && result) { - _decodedIFrames.add(frameInfo.frameSeq); - // 限制集合大小,避免内存泄漏 - if (_decodedIFrames.length > 30) { - _decodedIFrames.remove(_decodedIFrames.first); - } - } - } catch (e) { - AppLog.log('解码帧错误: $e, 帧序号: ${frameInfo.frameSeq}'); - } - } - // 新增:音频帧播放逻辑 void _playAudioFrames() { // 如果缓冲区为空或未达到目标大小,不进行播放 @@ -462,10 +555,20 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { // 初始化视频解码器 _initVideoDecoder(); + + // 初始化H264帧缓冲区 + state.h264FrameBuffer.clear(); + state.isProcessingFrame = false; + + _setupFrameRefresh(); } @override void onClose() { + _closeH264File(); + // 停止帧处理定时器 + _stopFrameProcessTimer(); + _stopPlayG711Data(); // 停止播放音频 state.audioBuffer.clear(); // 清空音频缓冲区 @@ -490,11 +593,9 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { _streamSubscription?.cancel(); _isListening = false; - // 停止FPS监测 - stopFpsMonitoring(); // 重置期望数据 StartChartManage().reSetDefaultTalkExpect(); - VideoDecodePlugin.releaseAllDecoders(); + VideoDecodePlugin.releaseDecoder(); // 取消批处理定时器 _batchProcessTimer?.cancel(); @@ -502,7 +603,8 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { // 清空已解码I帧集合 _decodedIFrames.clear(); - + _frameRefreshTimer?.cancel(); + _frameRefreshTimer = null; super.onClose(); } @@ -722,91 +824,430 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { return result; } - // 启动网络状态监测 - void startFpsMonitoring() { - // 确保只有一个计时器在运行 - stopFpsMonitoring(); - - // 初始化时间记录 - state.lastFpsUpdateTime.value = DateTime.now().millisecondsSinceEpoch; - - // 创建一个计时器,每秒更新一次丢包率和性能数据 - state.fpsTimer = Timer.periodic(const Duration(seconds: 1), (timer) { - // 更新丢包率数据 - updatePacketLossStats(); - - // 分析性能数据 - _analyzePerformance(); - }); - } - - // 停止网络状态监测 - void stopFpsMonitoring() { - state.fpsTimer?.cancel(); - state.fpsTimer = null; - } - - // 日志记录方法 - void logMessage(String message) { - AppLog.log(message); - } - - // 更新丢包率统计数据 - void updatePacketLossStats() async { - try {} catch (e) { - logMessage('获取丢包率数据失败: $e'); - } - } - - // 添加性能分析方法 - void _analyzePerformance() { - final int now = DateTime.now().millisecondsSinceEpoch; - - // 如果是首次调用,初始化数据 - if (state.lastPerformanceCheck == 0) { - state.lastPerformanceCheck = now; - state.lastFrameCount = state.renderedFrameCount.value; - return; - } - - // 每秒分析一次性能 - if (now - state.lastPerformanceCheck >= 1000) { - // 计算过去一秒的实际帧率 - final int frameRendered = - state.renderedFrameCount.value - state.lastFrameCount; - final double actualFPS = - frameRendered * 1000 / (now - state.lastPerformanceCheck); - - // 计算丢帧率 - final double dropRate = state.droppedFrames.value / - (state.totalFrames.value > 0 ? state.totalFrames.value : 1) * - 100; - - // 计算当前解码器积压帧数 - final int pendingFrames = - state.totalFrames.value - state.renderedFrameCount.value; - - // 计算跟踪Map中的帧数(正在处理中的帧) - final int processingFrames = state.frameTracker.length; - - // 分析渲染瓶颈 - String performanceStatus = "正常"; - if (actualFPS < 15 && dropRate > 10) { - performanceStatus = "严重渲染瓶颈"; - } else if (actualFPS < 20 && dropRate > 5) { - performanceStatus = "轻微渲染瓶颈"; + /// 追加写入一帧到h264文件(需传入帧数据和帧类型frameType) + Future _appendH264FrameToFile( + List frameData, TalkDataH264Frame_FrameTypeE frameType) async { + try { + if (state.h264File == null) { + await _initH264File(); + } + // NALU分割函数,返回每个NALU的完整字节数组 + List> splitNalus(List data) { + List> nalus = []; + int i = 0; + while (i < data.length - 3) { + int start = -1; + int next = -1; + if (data[i] == 0x00 && data[i + 1] == 0x00) { + if (data[i + 2] == 0x01) { + start = i; + i += 3; + } else if (i + 3 < data.length && + data[i + 2] == 0x00 && + data[i + 3] == 0x01) { + start = i; + i += 4; + } else { + i++; + continue; + } + next = i; + while (next < data.length - 3) { + if (data[next] == 0x00 && + data[next + 1] == 0x00 && + ((data[next + 2] == 0x01) || + (data[next + 2] == 0x00 && data[next + 3] == 0x01))) { + break; + } + next++; + } + nalus.add(data.sublist(start, next)); + i = next; + } else { + i++; + } + } + int nalusTotalLen = + nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0; + if (nalus.isEmpty && data.isNotEmpty) { + nalus.add(data); + } else if (nalus.isNotEmpty && nalusTotalLen < data.length) { + nalus.add(data.sublist(nalusTotalLen)); + } + return nalus; } - // 输出综合性能分析 - AppLog.log("性能分析: 实际帧率=${actualFPS.toStringAsFixed(1)}fps, " + - "丢帧率=${dropRate.toStringAsFixed(1)}%, " + - "待处理帧数=$pendingFrames, " + - "处理中帧数=$processingFrames, " + - "状态=$performanceStatus"); + // 优化:I帧前只缓存SPS/PPS/IDR,首次写入严格顺序 + if (!_hasWrittenFirstIFrame) { + final nalus = splitNalus(frameData); + List> spsList = []; + List> ppsList = []; + List> idrList = []; + for (final nalu in nalus) { + int offset = 0; + if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) { + if (nalu[2] == 0x01) + offset = 3; + else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4; + } + if (nalu.length > offset) { + int naluType = nalu[offset] & 0x1F; + if (naluType == 7) { + spsList.add(nalu); + AppLog.log('SPS内容: ' + + nalu + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join(' ')); + } else if (naluType == 8) { + ppsList.add(nalu); + AppLog.log('PPS内容: ' + + nalu + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join(' ')); + } else if (naluType == 5) { + idrList.add(nalu); + } + // 其他类型不缓存也不写入头部 + } + } + // 只在首次I帧写入前缓存,严格顺序写入 + if (spsList.isNotEmpty && ppsList.isNotEmpty && idrList.isNotEmpty) { + for (final sps in spsList) { + await _writeSingleFrameToFile(_ensureStartCode(sps)); + AppLog.log('写入顺序: SPS'); + } + for (final pps in ppsList) { + await _writeSingleFrameToFile(_ensureStartCode(pps)); + AppLog.log('写入顺序: PPS'); + } + for (final idr in idrList) { + await _writeSingleFrameToFile(_ensureStartCode(idr)); + AppLog.log('写入顺序: IDR'); + } + _hasWrittenFirstIFrame = true; + } else { + // 未收齐SPS/PPS/IDR则继续缓存,等待下次I帧 + if (spsList.isNotEmpty) _preIFrameCache.addAll(spsList); + if (ppsList.isNotEmpty) _preIFrameCache.addAll(ppsList); + if (idrList.isNotEmpty) _preIFrameCache.addAll(idrList); + } + } else { + // 首帧后只写入IDR和P帧 + final nalus = splitNalus(frameData); + for (final nalu in nalus) { + int offset = 0; + if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) { + if (nalu[2] == 0x01) + offset = 3; + else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4; + } + if (nalu.length > offset) { + int naluType = nalu[offset] & 0x1F; + if (naluType == 5) { + await _writeSingleFrameToFile(_ensureStartCode(nalu)); + // AppLog.log('写入顺序: IDR'); + } else if (naluType == 1) { + await _writeSingleFrameToFile(_ensureStartCode(nalu)); + // AppLog.log('写入顺序: P帧'); + } else if (naluType == 7) { + // AppLog.log('遇到新SPS,已忽略'); + } else if (naluType == 8) { + // AppLog.log('遇到新PPS,已忽略'); + } + // 其他类型不写入 + } + } + } + } catch (e) { + AppLog.log('写入H264帧到文件失败: $e'); + } + } - // 重置统计数据 - state.lastPerformanceCheck = now; - state.lastFrameCount = state.renderedFrameCount.value; + // 统一NALU起始码为0x00000001 + List _ensureStartCode(List nalu) { + if (nalu.length >= 4 && + nalu[0] == 0x00 && + nalu[1] == 0x00 && + nalu[2] == 0x00 && + nalu[3] == 0x01) { + return nalu; + } else if (nalu.length >= 3 && + nalu[0] == 0x00 && + nalu[1] == 0x00 && + nalu[2] == 0x01) { + return [0x00, 0x00, 0x00, 0x01] + nalu.sublist(3); + } else { + return [0x00, 0x00, 0x00, 0x01] + nalu; + } + } + + /// 实际写入单帧到文件(带NALU头判断) + Future _writeSingleFrameToFile(List frameData) async { + bool hasNaluHeader = false; + if (frameData.length >= 4 && + frameData[0] == 0x00 && + frameData[1] == 0x00 && + ((frameData[2] == 0x01) || + (frameData[2] == 0x00 && frameData[3] == 0x01))) { + hasNaluHeader = true; + } + if (hasNaluHeader) { + await state.h264File!.writeAsBytes(frameData, mode: FileMode.append); + } else { + final List naluHeader = [0x00, 0x00, 0x01]; + await state.h264File! + .writeAsBytes(naluHeader + frameData, mode: FileMode.append); + } + } + + /// 初始化h264文件 + Future _initH264File() async { + try { + if (state.h264File != null) return; + // 获取Download目录 + Directory? downloadsDir; + if (Platform.isAndroid) { + // Android 10+ 推荐用getExternalStorageDirectory() + downloadsDir = await getExternalStorageDirectory(); + // 兼容部分ROM,优先用Download + final downloadPath = '/storage/emulated/0/Download'; + if (Directory(downloadPath).existsSync()) { + downloadsDir = Directory(downloadPath); + } + } else { + downloadsDir = await getApplicationDocumentsDirectory(); + } + final filePath = + '${downloadsDir!.path}/video_${DateTime.now().millisecondsSinceEpoch}.h264'; + state.h264FilePath = filePath; + state.h264File = File(filePath); + if (!await state.h264File!.exists()) { + await state.h264File!.create(recursive: true); + } + AppLog.log('H264文件初始化: $filePath'); + } catch (e) { + AppLog.log('H264文件初始化失败: $e'); + } + } + + /// 关闭h264文件 + Future _closeH264File() async { + try { + if (state.h264File != null) { + AppLog.log('H264文件已关闭: ${state.h264FilePath ?? ''}'); + } + state.h264File = null; + state.h264FilePath = null; + _preIFrameCache.clear(); + _hasWrittenFirstIFrame = false; + } catch (e) { + AppLog.log('关闭H264文件时出错: $e'); + } + } + + /// 从I帧数据中分割NALU并将SPS/PPS优先放入缓冲区(用于缓冲区发送) + void _extractAndBufferSpsPpsForBuffer( + List frameData, int durationMs, int frameSeq, int frameSeqI) { + List> splitNalus(List data) { + List> nalus = []; + int i = 0; + while (i < data.length - 3) { + int start = -1; + int next = -1; + if (data[i] == 0x00 && data[i + 1] == 0x00) { + if (data[i + 2] == 0x01) { + start = i; + i += 3; + } else if (i + 3 < data.length && + data[i + 2] == 0x00 && + data[i + 3] == 0x01) { + start = i; + i += 4; + } else { + i++; + continue; + } + next = i; + while (next < data.length - 3) { + if (data[next] == 0x00 && + data[next + 1] == 0x00 && + ((data[next + 2] == 0x01) || + (data[next + 2] == 0x00 && data[next + 3] == 0x01))) { + break; + } + next++; + } + nalus.add(data.sublist(start, next)); + i = next; + } else { + i++; + } + } + int nalusTotalLen = + nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0; + if (nalus.isEmpty && data.isNotEmpty) { + nalus.add(data); + } else if (nalus.isNotEmpty && nalusTotalLen < data.length) { + nalus.add(data.sublist(nalusTotalLen)); + } + return nalus; + } + + final nalus = splitNalus(frameData); + for (final nalu in nalus) { + int offset = 0; + if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) { + if (nalu[2] == 0x01) + offset = 3; + else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4; + } + if (nalu.length > offset) { + int naluType = nalu[offset] & 0x1F; + if (naluType == 7) { + // SPS + hasSps = true; + // 只在首次或内容变化时更新缓存 + if (spsCache == null || !_listEquals(spsCache!, nalu)) { + spsCache = List.from(nalu); + } + } else if (naluType == 8) { + // PPS + hasPps = true; + if (ppsCache == null || !_listEquals(ppsCache!, nalu)) { + ppsCache = List.from(nalu); + } + } + } + } + } + + // 新增:List比较工具 + bool _listEquals(List a, List b) { + if (a.length != b.length) return false; + for (int i = 0; i < a.length; i++) { + if (a[i] != b[i]) return false; + } + return true; + } + + // 新增:I帧处理方法 + void _handleIFrameWithSpsPpsAndIdr(List frameData, int durationMs, int frameSeq, int frameSeqI) { + // 清空缓冲区,丢弃I帧前所有未处理帧(只保留SPS/PPS/I帧) + state.h264FrameBuffer.clear(); + _extractAndBufferSpsPpsForBuffer(frameData, durationMs, frameSeq, frameSeqI); + // 只要缓存有SPS/PPS就先写入,再写I帧本体(只写IDR) + if (spsCache == null || ppsCache == null) { + // 没有SPS/PPS缓存,丢弃本次I帧 + return; + } + // 先写入SPS/PPS + _addFrameToBuffer(spsCache!, TalkDataH264Frame_FrameTypeE.I, durationMs, frameSeq, frameSeqI); + _addFrameToBuffer(ppsCache!, TalkDataH264Frame_FrameTypeE.I, durationMs, frameSeq, frameSeqI); + // 分割I帧包,只写入IDR(type 5) + List> nalus = []; + int i = 0; + List data = frameData; + while (i < data.length - 3) { + int start = -1; + int next = -1; + if (data[i] == 0x00 && data[i + 1] == 0x00) { + if (data[i + 2] == 0x01) { + start = i; + i += 3; + } else if (i + 3 < data.length && data[i + 2] == 0x00 && data[i + 3] == 0x01) { + start = i; + i += 4; + } else { + i++; + continue; + } + next = i; + while (next < data.length - 3) { + if (data[next] == 0x00 && data[next + 1] == 0x00 && ((data[next + 2] == 0x01) || (data[next + 2] == 0x00 && data[next + 3] == 0x01))) { + break; + } + next++; + } + nalus.add(data.sublist(start, next)); + i = next; + } else { + i++; + } + } + int nalusTotalLen = nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0; + if (nalus.isEmpty && data.isNotEmpty) { + nalus.add(data); + } else if (nalus.isNotEmpty && nalusTotalLen < data.length) { + nalus.add(data.sublist(nalusTotalLen)); + } + for (final nalu in nalus) { + int offset = 0; + if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) { + if (nalu[2] == 0x01) + offset = 3; + else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4; + } + if (nalu.length > offset) { + int naluType = nalu[offset] & 0x1F; + if (naluType == 5) { + _addFrameToBuffer(nalu, TalkDataH264Frame_FrameTypeE.I, durationMs, frameSeq, frameSeqI); + } + } + } + } + + // 新增:P帧处理方法 + void _handlePFrame(List frameData, int durationMs, int frameSeq, int frameSeqI) { + // 只写入P帧(type 1) + List> nalus = []; + int i = 0; + List data = frameData; + while (i < data.length - 3) { + int start = -1; + int next = -1; + if (data[i] == 0x00 && data[i + 1] == 0x00) { + if (data[i + 2] == 0x01) { + start = i; + i += 3; + } else if (i + 3 < data.length && data[i + 2] == 0x00 && data[i + 3] == 0x01) { + start = i; + i += 4; + } else { + i++; + continue; + } + next = i; + while (next < data.length - 3) { + if (data[next] == 0x00 && data[next + 1] == 0x00 && ((data[next + 2] == 0x01) || (data[next + 2] == 0x00 && data[next + 3] == 0x01))) { + break; + } + next++; + } + nalus.add(data.sublist(start, next)); + i = next; + } else { + i++; + } + } + int nalusTotalLen = nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0; + if (nalus.isEmpty && data.isNotEmpty) { + nalus.add(data); + } else if (nalus.isNotEmpty && nalusTotalLen < data.length) { + nalus.add(data.sublist(nalusTotalLen)); + } + for (final nalu in nalus) { + int offset = 0; + if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) { + if (nalu[2] == 0x01) + offset = 3; + else if (nalu[2] == 0x00 && nalu[3] == 0x01) offset = 4; + } + if (nalu.length > offset) { + int naluType = nalu[offset] & 0x1F; + if (naluType == 1) { + _addFrameToBuffer(nalu, TalkDataH264Frame_FrameTypeE.P, durationMs, frameSeq, frameSeqI); + } + } } } } diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_page.dart b/lib/talk/starChart/views/native/talk_view_native_decode_page.dart index 3172a6a6..a5c69b15 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_page.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_page.dart @@ -56,6 +56,7 @@ class _TalkViewNativeDecodePageState extends State state.animationController.forward(); } }); + } @override @@ -71,6 +72,7 @@ class _TalkViewNativeDecodePageState extends State child: Stack( alignment: Alignment.center, children: [ + // 悬浮帧率统计信息条 Obx( () { final double screenWidth = MediaQuery.of(context).size.width; @@ -93,32 +95,84 @@ class _TalkViewNativeDecodePageState extends State final double scaleWidth = physicalWidth / rotatedImageWidth; final double scaleHeight = physicalHeight / rotatedImageHeight; max(scaleWidth, scaleHeight); // 选择较大的缩放比例 - - return state.isLoading.isTrue - ? Image.asset( - 'images/main/monitorBg.png', - width: screenWidth, - height: screenHeight, - fit: BoxFit.cover, - ) - : PopScope( - canPop: false, - child: RepaintBoundary( - key: state.globalKey, - child: SizedBox.expand( - child: RotatedBox( - // 解码器不支持硬件旋转,使用RotatedBox - quarterTurns: -1, - child: Texture( - textureId: state.textureId.value!, - filterQuality: FilterQuality.medium, + return Column( + children: [ + Expanded( + child: state.isLoading.isTrue + ? Image.asset( + 'images/main/monitorBg.png', + width: screenWidth, + height: screenHeight, + fit: BoxFit.cover, + ) + : PopScope( + canPop: false, + child: RepaintBoundary( + key: state.globalKey, + child: SizedBox.expand( + child: RotatedBox( + // 解码器不支持硬件旋转,使用RotatedBox + quarterTurns: -1, + child: Texture( + textureId: state.textureId.value!, + filterQuality: FilterQuality.medium, + ), + ), + ), ), ), - ), - ), - ); + ), + ], + ); }, ), + Positioned( + top: 300.h, + right: 20.w, + child: Obx(() => Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.network_check, color: Colors.redAccent, size: 18), + SizedBox(width: 6), + Text( + '接受服务端H264帧率/秒: ', + style: TextStyle(color: Colors.white, fontSize: 15), + ), + Text( + '${state.networkH264Fps.value}', + style: TextStyle(color: Colors.redAccent, fontSize: 16, fontWeight: FontWeight.bold), + ), + Text(' fps', style: TextStyle(color: Colors.white, fontSize: 13)), + ], + ), + SizedBox(height: 4), + Row( + children: [ + Icon(Icons.send, color: Colors.blueAccent, size: 18), + SizedBox(width: 6), + Text( + '送入Native帧率/秒: ', + style: TextStyle(color: Colors.white, fontSize: 15), + ), + Text( + '${state.nativeSendFps.value}', + style: TextStyle(color: Colors.blueAccent, fontSize: 16, fontWeight: FontWeight.bold), + ), + Text(' fps', style: TextStyle(color: Colors.white, fontSize: 13)), + ], + ), + ], + ), + )), + ), Obx(() => state.isLoading.isTrue ? Positioned( bottom: 310.h, @@ -127,102 +181,6 @@ class _TalkViewNativeDecodePageState extends State style: TextStyle(color: Colors.black, fontSize: 26.sp), )) : Container()), - Obx(() => state.textureId.value != null && state.showFps.value - ? Positioned( - top: ScreenUtil().statusBarHeight + 10.h, - right: 20.w, - child: Container( - padding: - EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.h), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.5), - borderRadius: BorderRadius.circular(5.h), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - // Text( - // 'FPS: ${state.decoderFps.value.toStringAsFixed(1)}', - // style: TextStyle( - // color: _getPacketLossColor( - // state.packetLossRate.value), - // fontSize: 20.sp, - // ), - // ), - Text( - '丢包率: ${state.packetLossRate.value.toStringAsFixed(1)}%', - style: TextStyle( - color: _getPacketLossColor( - state.packetLossRate.value), - fontSize: 20.sp, - ), - ), - Text( - '消息丢失: ${state.messageLossRate.value.toStringAsFixed(1)}%', - style: TextStyle( - color: _getPacketLossColor( - state.messageLossRate.value), - fontSize: 20.sp, - ), - ), - Divider( - color: Colors.white30, - height: 10.h, - thickness: 1), - Text( - '已渲染帧: ${state.renderedFrameCount.value}', - style: - TextStyle(color: Colors.white, fontSize: 18.sp), - ), - Text( - '总帧数: ${state.totalFrames.value}', - style: - TextStyle(color: Colors.white, fontSize: 18.sp), - ), - Text( - '丢弃帧: ${state.droppedFrames.value}', - style: - TextStyle(color: Colors.white, fontSize: 18.sp), - ), - Text( - 'IDR帧: ${state.hasSentIDR.value ? "已发送" : "未发送"}', - style: TextStyle( - color: state.hasSentIDR.value - ? Colors.green - : Colors.red, - fontSize: 18.sp), - ), - Text( - 'SPS: ${state.hasSentSPS.value ? "已发送" : "未发送"}', - style: TextStyle( - color: state.hasSentSPS.value - ? Colors.green - : Colors.red, - fontSize: 18.sp), - ), - Text( - 'PPS: ${state.hasSentPPS.value ? "已发送" : "未发送"}', - style: TextStyle( - color: state.hasSentPPS.value - ? Colors.green - : Colors.red, - fontSize: 18.sp), - ), - Text( - 'keyFrameInterval: ${state.keyFrameInterval.value}', - style: - TextStyle(color: Colors.green, fontSize: 18.sp), - ), - Text( - 'decodingJitterMs: ${state.decodingJitterMs.value}', - style: - TextStyle(color: Colors.green, fontSize: 18.sp), - ), - ], - ), - ), - ) - : Container()), Obx(() => state.isLoading.isFalse && state.oneMinuteTime.value > 0 ? Positioned( top: ScreenUtil().statusBarHeight + 75.h, diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_state.dart b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart index d98d5d34..6eef401e 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_state.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart @@ -106,4 +106,19 @@ class TalkViewNativeDecodeState { // 帧跟踪Map,记录每个提交的帧,key为textureId_frameSeq Map> frameTracker = {}; + + // H264帧缓冲区相关 + final List> h264FrameBuffer = >[]; // H264帧缓冲区,存储帧数据和类型 + final int maxFrameBufferSize = 25; // 最大缓冲区大小 + final int targetFps = 120; // 目标解码帧率 + Timer? frameProcessTimer; // 帧处理定时器 + bool isProcessingFrame = false; // 是否正在处理帧 + int lastProcessedTimestamp = 0; // 上次处理帧的时间戳 + // H264文件保存相关 + String? h264FilePath; + File? h264File; + + // 新增:用于页面显示的帧率统计 + RxInt networkH264Fps = 0.obs; + RxInt nativeSendFps = 0.obs; } From 357eaac746dfcc6957fec1cd4657d6906c4ea7d4 Mon Sep 17 00:00:00 2001 From: liyi Date: Wed, 30 Apr 2025 17:55:57 +0800 Subject: [PATCH 051/151] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0native=E8=A7=A3?= =?UTF-8?q?=E7=A0=81=E6=8F=92=E4=BB=B6=E6=94=AF=E6=8C=81720P=E5=AF=B9?= =?UTF-8?q?=E8=AE=B2=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/appRouters.dart | 4 +- .../starChart/constant/talk_constant.dart | 6 +- .../native/talk_view_native_decode_logic.dart | 270 +++++++----------- .../native/talk_view_native_decode_page.dart | 52 +--- .../native/talk_view_native_decode_state.dart | 8 +- 5 files changed, 115 insertions(+), 225 deletions(-) diff --git a/lib/appRouters.dart b/lib/appRouters.dart index 4990d37f..9043ffe1 100755 --- a/lib/appRouters.dart +++ b/lib/appRouters.dart @@ -1185,7 +1185,7 @@ abstract class AppRouters { page: () => const DoubleLockLinkPage()), GetPage( name: Routers.starChartTalkView, page: () => const TalkViewPage()), - // GetPage(name: Routers.h264WebView, page: () => TalkViewNativeDecodePage()), // 插件播放页面 - GetPage(name: Routers.h264WebView, page: () => H264WebView()), // webview播放页面 + GetPage(name: Routers.h264WebView, page: () => TalkViewNativeDecodePage()), // 插件播放页面 + // GetPage(name: Routers.h264WebView, page: () => H264WebView()), // webview播放页面 ]; } diff --git a/lib/talk/starChart/constant/talk_constant.dart b/lib/talk/starChart/constant/talk_constant.dart index 60d57a0d..e6f72519 100644 --- a/lib/talk/starChart/constant/talk_constant.dart +++ b/lib/talk/starChart/constant/talk_constant.dart @@ -13,7 +13,11 @@ class TalkConstant { audioType: [AudioTypeE.G711], ); static TalkExpectReq H264Expect = TalkExpectReq( - videoType: [VideoTypeE.H264], + videoType: [VideoTypeE.H264_720P], + audioType: [AudioTypeE.G711], + ); + static TalkExpectReq H264_720P_Expect = TalkExpectReq( + videoType: [VideoTypeE.H264_720P], audioType: [AudioTypeE.G711], ); } diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart index 8129f204..568da186 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart @@ -60,25 +60,10 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { // 添加一个集合来跟踪已成功解码的I帧序号 final Set _decodedIFrames = {}; - // 定义一个变量来保存上一帧的时间戳 - int? _previousFrameTimestamp; - - int _flutterToNativeFrameCount = 0; - int _lastFlutterToNativePrintTime = 0; - int _networkFrameCount = 0; - int _lastNetworkPrintTime = 0; - Timer? _frameRefreshTimer; - - bool _isFrameAvailable = true; - int _renderedFrameCount = 0; - int _lastRenderedFrameTime = 0; - // 写入前的缓存队列(I帧前) final List> _preIFrameCache = []; bool _hasWrittenFirstIFrame = false; - bool _isStartNative = false; - // 新增:SPS/PPS状态追踪变量 bool hasSps = false; bool hasPps = false; @@ -87,35 +72,13 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { List? spsCache; List? ppsCache; - void _setupFrameRefresh() { - // 设置帧刷新定时器,16ms对应约60fps - _frameRefreshTimer = - Timer.periodic(const Duration(milliseconds: 16), (timer) { - if (_isFrameAvailable) { - _isFrameAvailable = false; - _renderedFrameCount++; - - // 每秒统计一次帧率 - int now = DateTime.now().millisecondsSinceEpoch; - if (now - _lastRenderedFrameTime > 1000) { - print('[Flutter] 每秒渲染帧数: $_renderedFrameCount'); - _renderedFrameCount = 0; - _lastRenderedFrameTime = now; - } - - // 请求Flutter重建widget - WidgetsBinding.instance.scheduleFrame(); - } - }); - } - - void onFrameAvailable() { - _isFrameAvailable = true; - } + // 新增:记录上一个已接收的frameSeq + int? _lastFrameSeq; // 初始化视频解码器 Future _initVideoDecoder() async { try { + state.isLoading.value = true; // 创建解码器配置 final config = VideoDecoderConfig( width: 1280, @@ -128,10 +91,15 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { if (textureId != null) { state.textureId.value = textureId; AppLog.log('视频解码器初始化成功:textureId=$textureId'); - // 启动帧处理定时器 + + VideoDecodePlugin.setOnFrameRenderedListener((textureId) { + state.isLoading.value = false; + AppLog.log('已经开始渲染======='); + }); } else { AppLog.log('视频解码器初始化失败'); } + // 启动定时器发送帧数据 _startFrameProcessTimer(); } catch (e) { AppLog.log('初始化视频解码器错误: $e'); @@ -178,15 +146,13 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { int frameSeq, int frameSeqI, ) { - _networkFrameCount++; - int now = DateTime.now().millisecondsSinceEpoch; - if (now - _lastNetworkPrintTime > 1000) { - AppLog.log('[Flutter] 每秒收到网络H264帧数: ' + _networkFrameCount.toString()); - state.networkH264Fps.value = _networkFrameCount; - _networkFrameCount = 0; - _lastNetworkPrintTime = now; + // 只允许frameSeq严格递增,乱序或重复帧直接丢弃 + if (_lastFrameSeq != null && frameSeq <= _lastFrameSeq!) { + // 可选:打印日志 + AppLog.log('丢弃乱序或重复帧: frameSeq=$frameSeq, lastFrameSeq=$_lastFrameSeq'); + return; } - + _lastFrameSeq = frameSeq; // 创建包含帧数据和类型的Map final Map frameMap = { 'frameData': frameData, @@ -203,15 +169,6 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { while (state.h264FrameBuffer.length > state.maxFrameBufferSize) { state.h264FrameBuffer.removeAt(0); } - - _flutterToNativeFrameCount++; - if (now - _lastFlutterToNativePrintTime > 1000) { - AppLog.log( - '[Flutter] 每秒送入Native帧数: ' + _flutterToNativeFrameCount.toString()); - state.nativeSendFps.value = _flutterToNativeFrameCount; - _flutterToNativeFrameCount = 0; - _lastFlutterToNativePrintTime = now; - } } /// 启动帧处理定时器 @@ -225,12 +182,8 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { // 创建新定时器 state.frameProcessTimer = Timer.periodic(Duration(milliseconds: intervalMs), (timer) { - if (state.isLoading.isTrue) { - state.isLoading.value = false; - } _processNextFrameFromBuffer(); }); - AppLog.log('启动帧处理定时器,目标帧率: ${state.targetFps}fps,间隔: ${intervalMs}ms'); } @@ -258,40 +211,28 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { final int frameSeqI = frameMap['frameSeqI']; int pts = DateTime.now().millisecondsSinceEpoch; - if (frameType == TalkDataH264Frame_FrameTypeE.P) { - // 以frameSeqI为I帧序号标识 - if (!(_decodedIFrames.contains(frameSeqI))) { - AppLog.log('丢弃P帧:未收到对应I帧,frameSeqI=${frameSeqI}'); - return; - } - } else if (frameType == TalkDataH264Frame_FrameTypeE.I) { - // 记录已解码I帧序号 - _decodedIFrames.add(frameSeq); - } + // if (frameType == TalkDataH264Frame_FrameTypeE.P) { + // // 以frameSeqI为I帧序号标识 + // if (!(_decodedIFrames.contains(frameSeqI))) { + // AppLog.log('丢弃P帧:未收到对应I帧,frameSeqI=${frameSeqI}'); + // return; + // } + // } else if (frameType == TalkDataH264Frame_FrameTypeE.I) { + // // 记录已解码I帧序号 + // _decodedIFrames.add(frameSeq); + // } // 实时写入h264文件 - _appendH264FrameToFile(frameData, frameType); + // _appendH264FrameToFile(frameData, frameType); final timestamp = DateTime.now().microsecondsSinceEpoch; - VideoDecodePlugin.decodeFrame( - frameData: Uint8List.fromList(frameData), - frameType: frameType == TalkDataH264Frame_FrameTypeE.I ? 1 : 0, + VideoDecodePlugin.sendFrame( + frameData: frameData, + frameType: frameType == TalkDataH264Frame_FrameTypeE.I ? 0 : 1, frameSeq: frameSeq, timestamp: timestamp, + splitNalFromIFrame: true, refIFrameSeq: frameSeqI, ); - - // 判断P帧对应I帧是否已解码,未解码则丢弃P帧 - if (frameType == TalkDataH264Frame_FrameTypeE.P) { - // 以frameSeqI为I帧序号标识 - if (!(_decodedIFrames.contains(frameSeqI))) { - AppLog.log('丢弃P帧:未收到对应I帧,frameSeqI=${frameSeqI}'); - } - } else if (frameType == TalkDataH264Frame_FrameTypeE.I) { - // 记录已解码I帧序号 - _decodedIFrames.add(frameSeq); - } - // 实时写入h264文件 - _appendH264FrameToFile(frameData, frameType); } catch (e) { AppLog.log('处理缓冲帧失败: $e'); } finally { @@ -306,7 +247,6 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { state.frameProcessTimer = null; state.h264FrameBuffer.clear(); state.isProcessingFrame = false; - // AppLog.log('停止帧处理定时器'); } // 发起接听命令 @@ -342,54 +282,35 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { _playAudioFrames(); break; case TalkData_ContentTypeE.H264: - // if (_isStartNative) { - // if (talkDataH264Frame != null) { - // // 如果是I帧,先分割NALU,找到SPS/PPS并优先放入缓冲区 - // if (talkDataH264Frame.frameType == - // TalkDataH264Frame_FrameTypeE.I) { - // // 清空缓冲区,丢弃I帧前所有未处理帧(只保留SPS/PPS/I帧) - // state.h264FrameBuffer.clear(); - // _extractAndBufferSpsPpsForBuffer( - // talkData.content, - // talkData.durationMs, - // talkDataH264Frame.frameSeq, - // talkDataH264Frame.frameSeqI); - // } - // _addFrameToBuffer( - // talkData.content, - // talkDataH264Frame.frameType, - // talkData.durationMs, - // talkDataH264Frame.frameSeq, - // talkDataH264Frame.frameSeqI); - // } - // } else { - // await VideoDecodePlugin.startNativePlayer( - // VideoDecoderConfig(width: 1280, height: 720, codecType: 'h264'), - // ); - // _isStartNative = true; - // } // 处理H264帧 if (state.textureId.value != null) { if (talkDataH264Frame != null) { - if (talkDataH264Frame.frameType == - TalkDataH264Frame_FrameTypeE.I) { - _handleIFrameWithSpsPpsAndIdr( - talkData.content, - talkData.durationMs, - talkDataH264Frame.frameSeq, - talkDataH264Frame.frameSeqI, - ); - return; - } else if (talkDataH264Frame.frameType == - TalkDataH264Frame_FrameTypeE.P) { - _handlePFrame( - talkData.content, - talkData.durationMs, - talkDataH264Frame.frameSeq, - talkDataH264Frame.frameSeqI, - ); - return; - } + _addFrameToBuffer( + talkData.content, + talkDataH264Frame.frameType, + talkData.durationMs, + talkDataH264Frame.frameSeq, + talkDataH264Frame.frameSeqI, + ); + // if (talkDataH264Frame.frameType == + // TalkDataH264Frame_FrameTypeE.I) { + // _handleIFrameWithSpsPpsAndIdr( + // talkData.content, + // talkData.durationMs, + // talkDataH264Frame.frameSeq, + // talkDataH264Frame.frameSeqI, + // ); + // return; + // } else if (talkDataH264Frame.frameType == + // TalkDataH264Frame_FrameTypeE.P) { + // _handlePFrame( + // talkData.content, + // talkData.durationMs, + // talkDataH264Frame.frameSeq, + // talkDataH264Frame.frameSeqI, + // ); + // return; + // } } } else { AppLog.log('无法处理H264帧:textureId为空'); @@ -459,7 +380,7 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { /// 播放音频数据 void _playAudioData(TalkData talkData) async { - if (state.isOpenVoice.value) { + if (state.isOpenVoice.value && state.isLoading.isFalse) { final list = G711().decodeAndDenoise(talkData.content, true, 8000, 300, 150); // // 将 PCM 数据转换为 PcmArrayInt16 @@ -545,9 +466,6 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { // 初始化音频播放器 _initFlutterPcmSound(); - // 启动播放定时器 - // _startPlayback(); - // 初始化录音控制器 _initAudioRecorder(); @@ -559,13 +477,11 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { // 初始化H264帧缓冲区 state.h264FrameBuffer.clear(); state.isProcessingFrame = false; - - _setupFrameRefresh(); } @override void onClose() { - _closeH264File(); + // _closeH264File(); // 停止帧处理定时器 _stopFrameProcessTimer(); @@ -603,8 +519,7 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { // 清空已解码I帧集合 _decodedIFrames.clear(); - _frameRefreshTimer?.cancel(); - _frameRefreshTimer = null; + super.onClose(); } @@ -894,16 +809,16 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { int naluType = nalu[offset] & 0x1F; if (naluType == 7) { spsList.add(nalu); - AppLog.log('SPS内容: ' + - nalu - .map((b) => b.toRadixString(16).padLeft(2, '0')) - .join(' ')); + // AppLog.log('SPS内容: ' + + // nalu + // .map((b) => b.toRadixString(16).padLeft(2, '0')) + // .join(' ')); } else if (naluType == 8) { ppsList.add(nalu); - AppLog.log('PPS内容: ' + - nalu - .map((b) => b.toRadixString(16).padLeft(2, '0')) - .join(' ')); + // AppLog.log('PPS内容: ' + + // nalu + // .map((b) => b.toRadixString(16).padLeft(2, '0')) + // .join(' ')); } else if (naluType == 5) { idrList.add(nalu); } @@ -914,15 +829,15 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { if (spsList.isNotEmpty && ppsList.isNotEmpty && idrList.isNotEmpty) { for (final sps in spsList) { await _writeSingleFrameToFile(_ensureStartCode(sps)); - AppLog.log('写入顺序: SPS'); + // AppLog.log('写入顺序: SPS'); } for (final pps in ppsList) { await _writeSingleFrameToFile(_ensureStartCode(pps)); - AppLog.log('写入顺序: PPS'); + // AppLog.log('写入顺序: PPS'); } for (final idr in idrList) { await _writeSingleFrameToFile(_ensureStartCode(idr)); - AppLog.log('写入顺序: IDR'); + // AppLog.log('写入顺序: IDR'); } _hasWrittenFirstIFrame = true; } else { @@ -1131,18 +1046,22 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { } // 新增:I帧处理方法 - void _handleIFrameWithSpsPpsAndIdr(List frameData, int durationMs, int frameSeq, int frameSeqI) { + void _handleIFrameWithSpsPpsAndIdr( + List frameData, int durationMs, int frameSeq, int frameSeqI) { // 清空缓冲区,丢弃I帧前所有未处理帧(只保留SPS/PPS/I帧) state.h264FrameBuffer.clear(); - _extractAndBufferSpsPpsForBuffer(frameData, durationMs, frameSeq, frameSeqI); + _extractAndBufferSpsPpsForBuffer( + frameData, durationMs, frameSeq, frameSeqI); // 只要缓存有SPS/PPS就先写入,再写I帧本体(只写IDR) if (spsCache == null || ppsCache == null) { // 没有SPS/PPS缓存,丢弃本次I帧 return; } // 先写入SPS/PPS - _addFrameToBuffer(spsCache!, TalkDataH264Frame_FrameTypeE.I, durationMs, frameSeq, frameSeqI); - _addFrameToBuffer(ppsCache!, TalkDataH264Frame_FrameTypeE.I, durationMs, frameSeq, frameSeqI); + _addFrameToBuffer(spsCache!, TalkDataH264Frame_FrameTypeE.I, durationMs, + frameSeq, frameSeqI); + _addFrameToBuffer(ppsCache!, TalkDataH264Frame_FrameTypeE.I, durationMs, + frameSeq, frameSeqI); // 分割I帧包,只写入IDR(type 5) List> nalus = []; int i = 0; @@ -1154,7 +1073,9 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { if (data[i + 2] == 0x01) { start = i; i += 3; - } else if (i + 3 < data.length && data[i + 2] == 0x00 && data[i + 3] == 0x01) { + } else if (i + 3 < data.length && + data[i + 2] == 0x00 && + data[i + 3] == 0x01) { start = i; i += 4; } else { @@ -1163,7 +1084,10 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { } next = i; while (next < data.length - 3) { - if (data[next] == 0x00 && data[next + 1] == 0x00 && ((data[next + 2] == 0x01) || (data[next + 2] == 0x00 && data[next + 3] == 0x01))) { + if (data[next] == 0x00 && + data[next + 1] == 0x00 && + ((data[next + 2] == 0x01) || + (data[next + 2] == 0x00 && data[next + 3] == 0x01))) { break; } next++; @@ -1174,7 +1098,8 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { i++; } } - int nalusTotalLen = nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0; + int nalusTotalLen = + nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0; if (nalus.isEmpty && data.isNotEmpty) { nalus.add(data); } else if (nalus.isNotEmpty && nalusTotalLen < data.length) { @@ -1190,14 +1115,16 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { if (nalu.length > offset) { int naluType = nalu[offset] & 0x1F; if (naluType == 5) { - _addFrameToBuffer(nalu, TalkDataH264Frame_FrameTypeE.I, durationMs, frameSeq, frameSeqI); + _addFrameToBuffer(nalu, TalkDataH264Frame_FrameTypeE.I, durationMs, + frameSeq, frameSeqI); } } } } // 新增:P帧处理方法 - void _handlePFrame(List frameData, int durationMs, int frameSeq, int frameSeqI) { + void _handlePFrame( + List frameData, int durationMs, int frameSeq, int frameSeqI) { // 只写入P帧(type 1) List> nalus = []; int i = 0; @@ -1209,7 +1136,9 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { if (data[i + 2] == 0x01) { start = i; i += 3; - } else if (i + 3 < data.length && data[i + 2] == 0x00 && data[i + 3] == 0x01) { + } else if (i + 3 < data.length && + data[i + 2] == 0x00 && + data[i + 3] == 0x01) { start = i; i += 4; } else { @@ -1218,7 +1147,10 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { } next = i; while (next < data.length - 3) { - if (data[next] == 0x00 && data[next + 1] == 0x00 && ((data[next + 2] == 0x01) || (data[next + 2] == 0x00 && data[next + 3] == 0x01))) { + if (data[next] == 0x00 && + data[next + 1] == 0x00 && + ((data[next + 2] == 0x01) || + (data[next + 2] == 0x00 && data[next + 3] == 0x01))) { break; } next++; @@ -1229,7 +1161,8 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { i++; } } - int nalusTotalLen = nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0; + int nalusTotalLen = + nalus.isNotEmpty ? nalus.fold(0, (p, n) => p + n.length) : 0; if (nalus.isEmpty && data.isNotEmpty) { nalus.add(data); } else if (nalus.isNotEmpty && nalusTotalLen < data.length) { @@ -1245,7 +1178,8 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { if (nalu.length > offset) { int naluType = nalu[offset] & 0x1F; if (naluType == 1) { - _addFrameToBuffer(nalu, TalkDataH264Frame_FrameTypeE.P, durationMs, frameSeq, frameSeqI); + _addFrameToBuffer(nalu, TalkDataH264Frame_FrameTypeE.P, durationMs, + frameSeq, frameSeqI); } } } diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_page.dart b/lib/talk/starChart/views/native/talk_view_native_decode_page.dart index a5c69b15..75af93b2 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_page.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_page.dart @@ -13,6 +13,7 @@ import 'package:star_lock/talk/call/callTalk.dart'; import 'package:star_lock/talk/starChart/constant/talk_status.dart'; import 'package:star_lock/talk/starChart/handle/impl/debug_Info_model.dart'; import 'package:star_lock/talk/starChart/handle/impl/udp_talk_data_handler.dart'; +import 'package:star_lock/talk/starChart/star_chart_manage.dart'; import 'package:star_lock/talk/starChart/views/native/talk_view_native_decode_logic.dart'; import 'package:star_lock/talk/starChart/views/native/talk_view_native_decode_state.dart'; import 'package:star_lock/talk/starChart/views/talkView/talk_view_logic.dart'; @@ -35,6 +36,7 @@ class _TalkViewNativeDecodePageState extends State final TalkViewNativeDecodeLogic logic = Get.put(TalkViewNativeDecodeLogic()); final TalkViewNativeDecodeState state = Get.find().state; + final startChartManage = StartChartManage(); @override void initState() { @@ -112,7 +114,7 @@ class _TalkViewNativeDecodePageState extends State child: SizedBox.expand( child: RotatedBox( // 解码器不支持硬件旋转,使用RotatedBox - quarterTurns: -1, + quarterTurns: startChartManage.rotateAngle ~/ 90, child: Texture( textureId: state.textureId.value!, filterQuality: FilterQuality.medium, @@ -126,53 +128,7 @@ class _TalkViewNativeDecodePageState extends State ); }, ), - Positioned( - top: 300.h, - right: 20.w, - child: Obx(() => Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.5), - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(Icons.network_check, color: Colors.redAccent, size: 18), - SizedBox(width: 6), - Text( - '接受服务端H264帧率/秒: ', - style: TextStyle(color: Colors.white, fontSize: 15), - ), - Text( - '${state.networkH264Fps.value}', - style: TextStyle(color: Colors.redAccent, fontSize: 16, fontWeight: FontWeight.bold), - ), - Text(' fps', style: TextStyle(color: Colors.white, fontSize: 13)), - ], - ), - SizedBox(height: 4), - Row( - children: [ - Icon(Icons.send, color: Colors.blueAccent, size: 18), - SizedBox(width: 6), - Text( - '送入Native帧率/秒: ', - style: TextStyle(color: Colors.white, fontSize: 15), - ), - Text( - '${state.nativeSendFps.value}', - style: TextStyle(color: Colors.blueAccent, fontSize: 16, fontWeight: FontWeight.bold), - ), - Text(' fps', style: TextStyle(color: Colors.white, fontSize: 13)), - ], - ), - ], - ), - )), - ), + Obx(() => state.isLoading.isTrue ? Positioned( bottom: 310.h, diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_state.dart b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart index 6eef401e..e3408141 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_state.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart @@ -109,16 +109,12 @@ class TalkViewNativeDecodeState { // H264帧缓冲区相关 final List> h264FrameBuffer = >[]; // H264帧缓冲区,存储帧数据和类型 - final int maxFrameBufferSize = 25; // 最大缓冲区大小 - final int targetFps = 120; // 目标解码帧率 + final int maxFrameBufferSize = 15; // 最大缓冲区大小 + final int targetFps = 25; // 目标解码帧率,只是为了快速填充native的缓冲区 Timer? frameProcessTimer; // 帧处理定时器 bool isProcessingFrame = false; // 是否正在处理帧 int lastProcessedTimestamp = 0; // 上次处理帧的时间戳 // H264文件保存相关 String? h264FilePath; File? h264File; - - // 新增:用于页面显示的帧率统计 - RxInt networkH264Fps = 0.obs; - RxInt nativeSendFps = 0.obs; } From 07d20dfbf219431254b67ad8c43c6575cd43bd31 Mon Sep 17 00:00:00 2001 From: liyi Date: Tue, 6 May 2025 11:42:16 +0800 Subject: [PATCH 052/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E4=B8=BA?= =?UTF-8?q?=E5=8E=9F=E6=9C=89=E7=9A=84864*480=E5=AF=B9=E8=AE=B2=E8=A7=86?= =?UTF-8?q?=E9=A2=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/talk/starChart/constant/talk_constant.dart | 2 +- .../views/native/talk_view_native_decode_logic.dart | 12 +++++++----- .../views/native/talk_view_native_decode_state.dart | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/talk/starChart/constant/talk_constant.dart b/lib/talk/starChart/constant/talk_constant.dart index e6f72519..ff8a6727 100644 --- a/lib/talk/starChart/constant/talk_constant.dart +++ b/lib/talk/starChart/constant/talk_constant.dart @@ -13,7 +13,7 @@ class TalkConstant { audioType: [AudioTypeE.G711], ); static TalkExpectReq H264Expect = TalkExpectReq( - videoType: [VideoTypeE.H264_720P], + videoType: [VideoTypeE.H264], audioType: [AudioTypeE.G711], ); static TalkExpectReq H264_720P_Expect = TalkExpectReq( diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart index 568da186..5aa2ea36 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart @@ -81,9 +81,9 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { state.isLoading.value = true; // 创建解码器配置 final config = VideoDecoderConfig( - width: 1280, + width: 864, // 实际视频宽度 - height: 720, + height: 480, codecType: 'h264', ); // 初始化解码器并获取textureId @@ -209,7 +209,8 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { final TalkDataH264Frame_FrameTypeE frameType = frameMap['frameType']; final int frameSeq = frameMap['frameSeq']; final int frameSeqI = frameMap['frameSeqI']; - int pts = DateTime.now().millisecondsSinceEpoch; + int pts = frameMap['pts']; + // int pts = DateTime.now().millisecondsSinceEpoch; // if (frameType == TalkDataH264Frame_FrameTypeE.P) { // // 以frameSeqI为I帧序号标识 @@ -224,12 +225,13 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { // 实时写入h264文件 // _appendH264FrameToFile(frameData, frameType); - final timestamp = DateTime.now().microsecondsSinceEpoch; + // final timestamp = DateTime.now().millisecondsSinceEpoch; + // final timestamp64 = timestamp is int ? timestamp : timestamp.toInt(); VideoDecodePlugin.sendFrame( frameData: frameData, frameType: frameType == TalkDataH264Frame_FrameTypeE.I ? 0 : 1, frameSeq: frameSeq, - timestamp: timestamp, + timestamp: pts, splitNalFromIFrame: true, refIFrameSeq: frameSeqI, ); diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_state.dart b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart index e3408141..9528671c 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_state.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart @@ -109,8 +109,8 @@ class TalkViewNativeDecodeState { // H264帧缓冲区相关 final List> h264FrameBuffer = >[]; // H264帧缓冲区,存储帧数据和类型 - final int maxFrameBufferSize = 15; // 最大缓冲区大小 - final int targetFps = 25; // 目标解码帧率,只是为了快速填充native的缓冲区 + final int maxFrameBufferSize = 30; // 最大缓冲区大小 + final int targetFps = 30; // 目标解码帧率,只是为了快速填充native的缓冲区 Timer? frameProcessTimer; // 帧处理定时器 bool isProcessingFrame = false; // 是否正在处理帧 int lastProcessedTimestamp = 0; // 上次处理帧的时间戳 From e1bf2f2fdce3864dab18c3e8d64640df14642c8a Mon Sep 17 00:00:00 2001 From: liyi Date: Tue, 6 May 2025 14:00:38 +0800 Subject: [PATCH 053/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E4=B8=BA?= =?UTF-8?q?=E5=8E=9F=E6=9C=89=E7=9A=84864*480=E5=AF=B9=E8=AE=B2=E8=A7=86?= =?UTF-8?q?=E9=A2=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../native/talk_view_native_decode_logic.dart | 21 +------------------ .../native/talk_view_native_decode_state.dart | 2 +- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart index 5aa2ea36..193c7379 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart @@ -209,7 +209,7 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { final TalkDataH264Frame_FrameTypeE frameType = frameMap['frameType']; final int frameSeq = frameMap['frameSeq']; final int frameSeqI = frameMap['frameSeqI']; - int pts = frameMap['pts']; + int pts = frameMap['pts']; // int pts = DateTime.now().millisecondsSinceEpoch; // if (frameType == TalkDataH264Frame_FrameTypeE.P) { @@ -294,25 +294,6 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { talkDataH264Frame.frameSeq, talkDataH264Frame.frameSeqI, ); - // if (talkDataH264Frame.frameType == - // TalkDataH264Frame_FrameTypeE.I) { - // _handleIFrameWithSpsPpsAndIdr( - // talkData.content, - // talkData.durationMs, - // talkDataH264Frame.frameSeq, - // talkDataH264Frame.frameSeqI, - // ); - // return; - // } else if (talkDataH264Frame.frameType == - // TalkDataH264Frame_FrameTypeE.P) { - // _handlePFrame( - // talkData.content, - // talkData.durationMs, - // talkDataH264Frame.frameSeq, - // talkDataH264Frame.frameSeqI, - // ); - // return; - // } } } else { AppLog.log('无法处理H264帧:textureId为空'); diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_state.dart b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart index 9528671c..d7039c52 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_state.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart @@ -109,7 +109,7 @@ class TalkViewNativeDecodeState { // H264帧缓冲区相关 final List> h264FrameBuffer = >[]; // H264帧缓冲区,存储帧数据和类型 - final int maxFrameBufferSize = 30; // 最大缓冲区大小 + final int maxFrameBufferSize = 17; // 最大缓冲区大小 final int targetFps = 30; // 目标解码帧率,只是为了快速填充native的缓冲区 Timer? frameProcessTimer; // 帧处理定时器 bool isProcessingFrame = false; // 是否正在处理帧 From 66dd5de9f10324af58702c33dfe3deabdc616763 Mon Sep 17 00:00:00 2001 From: liyi Date: Tue, 6 May 2025 16:43:28 +0800 Subject: [PATCH 054/151] =?UTF-8?q?fix:SKY=E7=8E=AF=E5=A2=83=E4=B8=8B?= =?UTF-8?q?=E4=B8=8D=E6=98=BE=E7=A4=BA=E8=AE=BE=E7=BD=AE=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=91=98=E5=BC=80=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../card/cardDetail/cardDetail_page.dart | 24 +++++++++++++------ .../fingerprintDetail_page.dart | 24 +++++++++++++------ .../passwordKeyDetail_page.dart | 3 ++- 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/lib/main/lockDetail/card/cardDetail/cardDetail_page.dart b/lib/main/lockDetail/card/cardDetail/cardDetail_page.dart index bc2fd663..8d8140e6 100755 --- a/lib/main/lockDetail/card/cardDetail/cardDetail_page.dart +++ b/lib/main/lockDetail/card/cardDetail/cardDetail_page.dart @@ -5,6 +5,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; +import 'package:star_lock/flavors.dart'; import 'package:star_lock/main/lockDetail/card/cardDetail/cardDetail_state.dart'; import '../../../../appRouters.dart'; @@ -179,13 +180,22 @@ class _CardDetailPageState extends State with RouteAware { isHaveRightWidget: true, rightWidget: SizedBox( width: 60.w, height: 50.h, child: _isStressFingerprint()))), - Obx(() => CommonItem( - leftTitel: '是否为管理员'.tr, - rightTitle: '', - isTipsImg: false, - isHaveRightWidget: true, - rightWidget: - SizedBox(width: 60.w, height: 50.h, child: _isAdmin()))), + Visibility( + visible: !F.isSKY, + child: Obx( + () => CommonItem( + leftTitel: '是否为管理员'.tr, + rightTitle: '', + isTipsImg: false, + isHaveRightWidget: true, + rightWidget: SizedBox( + width: 60.w, + height: 50.h, + child: _isAdmin(), + ), + ), + ), + ), Container(height: 10.h), CommonItem( leftTitel: '操作记录'.tr, diff --git a/lib/main/lockDetail/fingerprint/fingerprintDetail/fingerprintDetail_page.dart b/lib/main/lockDetail/fingerprint/fingerprintDetail/fingerprintDetail_page.dart index b3f66632..fc018b40 100755 --- a/lib/main/lockDetail/fingerprint/fingerprintDetail/fingerprintDetail_page.dart +++ b/lib/main/lockDetail/fingerprint/fingerprintDetail/fingerprintDetail_page.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; +import 'package:star_lock/flavors.dart'; import 'package:star_lock/main/lockDetail/fingerprint/fingerprintDetail/fingerprintDetail_state.dart'; import '../../../../appRouters.dart'; @@ -178,13 +179,22 @@ class _FingerprintDetailPageState extends State isHaveLine: true, rightWidget: SizedBox( width: 60.w, height: 50.h, child: _isStressFingerprint()))), - Obx(() => CommonItem( - leftTitel: '是否为管理员'.tr, - rightTitle: '', - isTipsImg: false, - isHaveRightWidget: true, - rightWidget: - SizedBox(width: 60.w, height: 50.h, child: _isAdmin()))), + Visibility( + visible: !F.isSKY, + child: Obx( + () => CommonItem( + leftTitel: '是否为管理员'.tr, + rightTitle: '', + isTipsImg: false, + isHaveRightWidget: true, + rightWidget: SizedBox( + width: 60.w, + height: 50.h, + child: _isAdmin(), + ), + ), + ), + ), Container(height: 10.h), CommonItem( leftTitel: '操作记录'.tr, diff --git a/lib/main/lockDetail/passwordKey/passwordKeyDetail/passwordKeyDetail_page.dart b/lib/main/lockDetail/passwordKey/passwordKeyDetail/passwordKeyDetail_page.dart index cc02af47..b9a39d6c 100755 --- a/lib/main/lockDetail/passwordKey/passwordKeyDetail/passwordKeyDetail_page.dart +++ b/lib/main/lockDetail/passwordKey/passwordKeyDetail/passwordKeyDetail_page.dart @@ -4,6 +4,7 @@ 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/flavors.dart'; import 'package:star_lock/main/lockDetail/passwordKey/passwordKeyDetail/passwordKeyDetail_logic.dart'; import 'package:star_lock/main/lockDetail/passwordKey/passwordKeyDetail/passwordKeyDetail_state.dart'; import 'package:star_lock/main/lockDetail/passwordKey/passwordKeyList/passwordKeyListEntity.dart'; @@ -133,7 +134,7 @@ class _PasswordKeyDetailPageState extends State action: () {}), Container(height: 10.h), Obx(() => Visibility( - visible: state.itemData.value.isCustom! == 1, + visible: state.itemData.value.isCustom! == 1 && !F.isSKY, child: Column( children: [ CommonItem( From 7ea26948782ac1f9880a617e1098f9b4b79c9bb5 Mon Sep 17 00:00:00 2001 From: liyi Date: Tue, 6 May 2025 16:43:47 +0800 Subject: [PATCH 055/151] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E8=83=81?= =?UTF-8?q?=E8=BF=AB=E9=80=9A=E7=9F=A5=E5=88=97=E8=A1=A8=E6=97=B6=E7=9A=84?= =?UTF-8?q?=E6=96=87=E6=9C=AC=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../coerceOpenDoor/coerceOpenDoor/coerceOpenDoor_logic.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/main/lockDetail/messageWarn/msgNotification/coerceOpenDoor/coerceOpenDoor/coerceOpenDoor_logic.dart b/lib/main/lockDetail/messageWarn/msgNotification/coerceOpenDoor/coerceOpenDoor/coerceOpenDoor_logic.dart index e646ffb2..ff350d42 100755 --- a/lib/main/lockDetail/messageWarn/msgNotification/coerceOpenDoor/coerceOpenDoor/coerceOpenDoor_logic.dart +++ b/lib/main/lockDetail/messageWarn/msgNotification/coerceOpenDoor/coerceOpenDoor/coerceOpenDoor_logic.dart @@ -29,9 +29,9 @@ class CoerceOpenDoorLogic extends BaseGetXController { case 2: return '密码'.tr; case 3: - return '指纹'.tr; + return 'IC卡'.tr; case 4: - return '卡'.tr; + return '指纹'.tr; case 5: return '人脸'.tr; default: From c84e05317252094cc489576e98333b4a46d1e69b Mon Sep 17 00:00:00 2001 From: liyi Date: Tue, 6 May 2025 16:44:02 +0800 Subject: [PATCH 056/151] =?UTF-8?q?fix:SKY=E7=8E=AF=E5=A2=83=E4=B8=8B?= =?UTF-8?q?=E4=B8=8D=E6=98=BE=E7=A4=BA=E8=AE=BE=E7=BD=AE=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=91=98=E5=BC=80=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../face/faceDetail/faceDetail_page.dart | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/lib/main/lockDetail/face/faceDetail/faceDetail_page.dart b/lib/main/lockDetail/face/faceDetail/faceDetail_page.dart index 766e81ab..e4af7a36 100755 --- a/lib/main/lockDetail/face/faceDetail/faceDetail_page.dart +++ b/lib/main/lockDetail/face/faceDetail/faceDetail_page.dart @@ -5,6 +5,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:get/get.dart'; +import 'package:star_lock/flavors.dart'; import 'package:star_lock/main/lockDetail/face/faceDetail/faceDetail_logic.dart'; import 'package:star_lock/main/lockDetail/face/faceDetail/faceDetail_state.dart'; @@ -174,13 +175,22 @@ class _FaceDetailPageState extends State with RouteAware { // isHaveRightWidget: true, // isHaveLine: true, // rightWidget: SizedBox(width: 60.w, height: 50.h, child: _isStressFace()))), - Obx(() => CommonItem( - leftTitel: '是否为管理员'.tr, - rightTitle: '', - isTipsImg: false, - isHaveRightWidget: true, - rightWidget: - SizedBox(width: 60.w, height: 50.h, child: _isAdmin()))), + Visibility( + visible: !F.isSKY, + child: Obx( + () => CommonItem( + leftTitel: '是否为管理员'.tr, + rightTitle: '', + isTipsImg: false, + isHaveRightWidget: true, + rightWidget: SizedBox( + width: 60.w, + height: 50.h, + child: _isAdmin(), + ), + ), + ), + ), Container(height: 10.h), CommonItem( leftTitel: '操作记录'.tr, From 2fbb4695b470e43ef972f140a7ee46c25c72ab05 Mon Sep 17 00:00:00 2001 From: liyi Date: Tue, 6 May 2025 16:44:21 +0800 Subject: [PATCH 057/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E7=8C=AB?= =?UTF-8?q?=E7=9C=BC=E7=9B=91=E6=8E=A7=E6=94=AF=E6=8C=81=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/main/lockDetail/lockDetail/lockDetail_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/main/lockDetail/lockDetail/lockDetail_page.dart b/lib/main/lockDetail/lockDetail/lockDetail_page.dart index ec09e9a3..ed3c9b68 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_page.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_page.dart @@ -1179,7 +1179,7 @@ class _LockDetailPageState extends State } //可视对讲门锁新增->监控 - if (state.keyInfos.value.lockFeature!.videoIntercom == 1) { + if (state.keyInfos.value.lockFeature!.isSupportCatEye == 1) { showWidgetArr.add( bottomItem('images/main/icon_catEyes.png', '监控'.tr, state.bottomBtnisEable.value, () async { From 723a803e179306a66ca8d467eb11f1d78791daa0 Mon Sep 17 00:00:00 2001 From: liyi Date: Wed, 7 May 2025 15:06:57 +0800 Subject: [PATCH 058/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4ios=E7=AB=AF?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E7=99=BD=E8=BE=B9=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../native/talk_view_native_decode_page.dart | 61 ++++++++++--------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_page.dart b/lib/talk/starChart/views/native/talk_view_native_decode_page.dart index 75af93b2..b7c1fb12 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_page.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_page.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:math'; +import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -58,7 +59,6 @@ class _TalkViewNativeDecodePageState extends State state.animationController.forward(); } }); - } @override @@ -97,35 +97,40 @@ class _TalkViewNativeDecodePageState extends State final double scaleWidth = physicalWidth / rotatedImageWidth; final double scaleHeight = physicalHeight / rotatedImageHeight; max(scaleWidth, scaleHeight); // 选择较大的缩放比例 - return Column( - children: [ - Expanded( - child: state.isLoading.isTrue - ? Image.asset( - 'images/main/monitorBg.png', - width: screenWidth, - height: screenHeight, - fit: BoxFit.cover, - ) - : PopScope( - canPop: false, - child: RepaintBoundary( - key: state.globalKey, - child: SizedBox.expand( - child: RotatedBox( - // 解码器不支持硬件旋转,使用RotatedBox - quarterTurns: startChartManage.rotateAngle ~/ 90, - child: Texture( - textureId: state.textureId.value!, - filterQuality: FilterQuality.medium, - ), - ), - ), + return state.isLoading.isTrue + ? Image.asset( + 'images/main/monitorBg.png', + width: screenWidth, + height: screenHeight, + fit: BoxFit.cover, + ) + : Positioned.fill( + child: PopScope( + canPop: false, + child: RepaintBoundary( + key: state.globalKey, + child: SizedBox.expand( + child: RotatedBox( + // 解码器不支持硬件旋转,使用RotatedBox + quarterTurns: + startChartManage.rotateAngle ~/ 90, + child: Platform.isIOS + ? Transform.scale( + scale: 1.008, // 轻微放大,消除iOS白边 + child: Texture( + textureId: state.textureId.value!, + filterQuality: FilterQuality.medium, + ), + ) + : Texture( + textureId: state.textureId.value!, + filterQuality: FilterQuality.medium, + ), ), ), - ), - ], - ); + ), + ), + ); }, ), From 6ef92e2340c717330b80f7c3b444d98f54978400 Mon Sep 17 00:00:00 2001 From: liyi Date: Wed, 7 May 2025 15:25:14 +0800 Subject: [PATCH 059/151] =?UTF-8?q?fix:=E5=8F=96=E6=B6=88=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0=E7=8A=B6=E6=80=81=E6=A0=8F=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/talk/starChart/handle/impl/udp_talk_request_handler.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart index 3a26c1fa..55a2ac27 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart @@ -85,7 +85,7 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle //test:使用自定义铃声 playRingtone(); // 显示状态栏弹窗 - _showTalkRequestNotification(talkObjectName: talkObjectName); + // _showTalkRequestNotification(talkObjectName: talkObjectName); // 设置为等待接听状态 talkStatus.setPassiveCallWaitingAnswer(); if (startChartManage From 2f03a98cc46d9dfcd9a5815413468ec86c68712a Mon Sep 17 00:00:00 2001 From: liyi Date: Wed, 7 May 2025 16:32:57 +0800 Subject: [PATCH 060/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E8=A7=A3?= =?UTF-8?q?=E7=A0=81=E6=8F=92=E4=BB=B6=E4=BE=9D=E8=B5=96=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pubspec.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 331117fb..91984c58 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -128,7 +128,9 @@ dependencies: aliyun_face_plugin: path: aliyun_face_plugin video_decode_plugin: - path: ../video_decode_plugin + git: + url: git@code.star-lock.cn:liyi/video_decode_plugin.git + ref: 73afceeedc74a3c987716c9956cbcc495d5650c3 flutter_localizations: sdk: flutter From 34da68e16cd2f7c586e8359eff7906613898cbfa Mon Sep 17 00:00:00 2001 From: liyi Date: Thu, 8 May 2025 10:14:34 +0800 Subject: [PATCH 061/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E7=BC=93?= =?UTF-8?q?=E5=86=B2=E5=8C=BA=E5=A4=A7=E5=B0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../starChart/views/native/talk_view_native_decode_logic.dart | 2 +- .../starChart/views/native/talk_view_native_decode_state.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart index 193c7379..f416f98b 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart @@ -227,7 +227,7 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { // final timestamp = DateTime.now().millisecondsSinceEpoch; // final timestamp64 = timestamp is int ? timestamp : timestamp.toInt(); - VideoDecodePlugin.sendFrame( + await VideoDecodePlugin.sendFrame( frameData: frameData, frameType: frameType == TalkDataH264Frame_FrameTypeE.I ? 0 : 1, frameSeq: frameSeq, diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_state.dart b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart index d7039c52..8703012d 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_state.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart @@ -109,7 +109,7 @@ class TalkViewNativeDecodeState { // H264帧缓冲区相关 final List> h264FrameBuffer = >[]; // H264帧缓冲区,存储帧数据和类型 - final int maxFrameBufferSize = 17; // 最大缓冲区大小 + final int maxFrameBufferSize = 7; // 最大缓冲区大小 final int targetFps = 30; // 目标解码帧率,只是为了快速填充native的缓冲区 Timer? frameProcessTimer; // 帧处理定时器 bool isProcessingFrame = false; // 是否正在处理帧 From 4e6f546019ec95b3977ac87a51a1ee66b73a32de Mon Sep 17 00:00:00 2001 From: liyi Date: Thu, 8 May 2025 10:14:53 +0800 Subject: [PATCH 062/151] =?UTF-8?q?fix:=E6=8F=92=E4=BB=B6=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 91984c58..119e3d25 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -130,7 +130,7 @@ dependencies: video_decode_plugin: git: url: git@code.star-lock.cn:liyi/video_decode_plugin.git - ref: 73afceeedc74a3c987716c9956cbcc495d5650c3 + ref: 38df1883f5108ec1ce590ba52318815333fded38 flutter_localizations: sdk: flutter From 1a67783d7a0f244709e1c421c9fc70d3aeccdafa Mon Sep 17 00:00:00 2001 From: liyi Date: Thu, 8 May 2025 11:33:38 +0800 Subject: [PATCH 063/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4mjpeg=E7=9A=84?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../views/talkView/talk_view_logic.dart | 178 +++++------------- .../views/talkView/talk_view_page.dart | 74 ++++---- .../views/talkView/talk_view_state.dart | 3 +- 3 files changed, 82 insertions(+), 173 deletions(-) diff --git a/lib/talk/starChart/views/talkView/talk_view_logic.dart b/lib/talk/starChart/views/talkView/talk_view_logic.dart index cc959c0b..02ed3f25 100644 --- a/lib/talk/starChart/views/talkView/talk_view_logic.dart +++ b/lib/talk/starChart/views/talkView/talk_view_logic.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:ui' as ui; import 'dart:math'; // Import the math package to use sqrt +import 'dart:ui' show decodeImageFromList; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; @@ -38,36 +39,25 @@ class TalkViewLogic extends BaseGetXController { final LockDetailState lockDetailState = Get.put(LockDetailLogic()).state; - final int minBufferSize = 2; // 最小缓冲2帧,约166ms - final int maxBufferSize = 20; // 最大缓冲8帧,约666ms - int bufferSize = 8; // 初始化为默认大小 - // 修改音频相关的成员变量 - final int minAudioBufferSize = 1; // 音频最小缓冲1帧 - final int maxAudioBufferSize = 3; // 音频最大缓冲3帧 + int bufferSize = 8; // 增大缓冲区,满时才渲染 + int audioBufferSize = 2; // 音频默认缓冲2帧 bool _isFirstAudioFrame = true; // 是否是第一帧 - // 添加开始时间记录 - int _startTime = 0; // 开始播放时间戳 + int _startAudioTime = 0; // 开始播放时间戳 - bool _isFirstFrame = true; // 是否是第一帧 // 定义音频帧缓冲和发送函数 final List _bufferedAudioFrames = []; - final Map _imageCache = {}; - - // 添加一个变量用于记录上一帧的时间戳 - int _lastFrameTimestamp = 0; // 初始值为 0 - - // 添加帧率计算相关变量 - int _frameCount = 0; - int _lastFpsUpdateTime = 0; - Timer? _fpsTimer; - // 添加监听状态和订阅引用 bool _isListening = false; StreamSubscription? _streamSubscription; + Timer? videoRenderTimer; // 视频渲染定时器 + + int _renderedFrameCount = 0; + int _lastFpsPrintTime = DateTime.now().millisecondsSinceEpoch; + /// 初始化音频播放器 void _initFlutterPcmSound() { const int sampleRate = 8000; @@ -143,79 +133,16 @@ class TalkViewLogic extends BaseGetXController { _playAudioFrames(); break; case TalkData_ContentTypeE.Image: - // 第一帧到达时记录开始时间 - if (_isFirstFrame) { - _startTime = currentTime; - _isFirstFrame = false; - // AppLog.log('第一帧帧的时间戳:${talkData.durationMs}'); - } - // AppLog.log('其他帧的时间戳:${talkData.durationMs}'); - // 计算帧间间隔 - if (_lastFrameTimestamp != 0) { - final int frameInterval = talkData.durationMs - _lastFrameTimestamp; - _adjustBufferSize(frameInterval); // 根据帧间间隔调整缓冲区 - } - _lastFrameTimestamp = talkData.durationMs; // 更新上一帧时间戳 - - // 然后添加到播放缓冲区 - if (state.videoBuffer.length >= bufferSize) { - state.videoBuffer.removeAt(0); - } + // 固定长度缓冲区,最多保留bufferSize帧 state.videoBuffer.add(talkData); - // 先进行解码和缓存 - await _decodeAndCacheFrame(talkData); - // 最后尝试播放 - _playVideoFrames(); + if (state.videoBuffer.length > bufferSize) { + state.videoBuffer.removeAt(0); // 移除最旧帧 + } break; } }); } - // 修改:视频帧播放逻辑 - void _playVideoFrames() { - // 如果缓冲区为空或未达到目标大小,不进行播放 - if (state.videoBuffer.isEmpty || state.videoBuffer.length < bufferSize) { - // AppLog.log('📊 缓冲中 - 当前缓冲区大小: ${state.videoBuffer.length}/${bufferSize}'); - return; - } - // 找出时间戳最小的帧(最旧的帧) - TalkData? oldestFrame; - int oldestIndex = -1; - for (int i = 0; i < state.videoBuffer.length; i++) { - if (oldestFrame == null || - state.videoBuffer[i].durationMs < oldestFrame.durationMs) { - oldestFrame = state.videoBuffer[i]; - oldestIndex = i; - } - } - // 确保找到了有效帧 - if (oldestFrame != null && oldestIndex != -1) { - final cacheKey = oldestFrame.content.hashCode.toString(); - - // 使用缓存的解码图片更新显示 - if (_imageCache.containsKey(cacheKey)) { - state.currentImage.value = _imageCache[cacheKey]; - state.listData.value = Uint8List.fromList(oldestFrame.content); - state.videoBuffer.removeAt(oldestIndex); // 移除已播放的帧 - - // // 更新帧率计算 - // _frameCount++; - // final currentTime = DateTime.now().millisecondsSinceEpoch; - // final elapsed = currentTime - _lastFpsUpdateTime; - // - // if (elapsed >= 1000) { - // // 每秒更新一次 - // state.fps.value = (_frameCount * 1000 / elapsed).round(); - // _frameCount = 0; - // _lastFpsUpdateTime = currentTime; - // } - } else { - // AppLog.log('⚠️ 帧未找到缓存 - Key: $cacheKey'); - state.videoBuffer.removeAt(oldestIndex); // 移除无法播放的帧 - } - } - } - // 新增:音频帧播放逻辑 void _playAudioFrames() { // 如果缓冲区为空或未达到目标大小,不进行播放 @@ -246,50 +173,6 @@ class TalkViewLogic extends BaseGetXController { } } - // 新增:解码和缓存帧的方法 - Future _decodeAndCacheFrame(TalkData talkData) async { - try { - String cacheKey = talkData.content.hashCode.toString(); - - // 如果该帧还没有被缓存,则进行解码和缓存 - if (!_imageCache.containsKey(cacheKey)) { - final Uint8List uint8Data = Uint8List.fromList(talkData.content); - final ui.Image image = await decodeImageFromList(uint8Data); - - // 管理缓存大小 - if (_imageCache.length >= bufferSize) { - _imageCache.remove(_imageCache.keys.first); - } - - // 添加到缓存 - _imageCache[cacheKey] = image; - - // AppLog.log('📥 缓存新帧 - 缓存数: ${_imageCache.length}, Key: $cacheKey'); - } - } catch (e) { - AppLog.log('❌ 帧解码错误: $e'); - } - } - - // 新增:动态调整缓冲区大小的方法 - void _adjustBufferSize(int frameInterval) { - const int frameDuration = 83; // 假设每帧的时间间隔为 83ms(12fps) - const int delayThresholdHigh = frameDuration * 2; // 高延迟阈值(2帧时间) - const int delayThresholdLow = frameDuration; // 低延迟阈值(1帧时间) - const int adjustInterval = 1; // 每次调整1帧 - - if (frameInterval > delayThresholdHigh && bufferSize < maxBufferSize) { - // 帧间间隔较大,增加缓冲区 - bufferSize = min(bufferSize + adjustInterval, maxBufferSize); - AppLog.log('📈 增加缓冲区 - 当前大小: $bufferSize, 帧间间隔: ${frameInterval}ms'); - } else if (frameInterval < delayThresholdLow && - bufferSize > minBufferSize) { - // 帧间间隔较小,减少缓冲区 - bufferSize = max(bufferSize - adjustInterval, minBufferSize); - AppLog.log('📉 减少缓冲区 - 当前大小: $bufferSize, 帧间间隔: ${frameInterval}ms'); - } - } - /// 监听对讲状态 void _startListenTalkStatus() { state.startChartTalkStatus.statusStream.listen((talkStatus) { @@ -496,6 +379,32 @@ class TalkViewLogic extends BaseGetXController { _initAudioRecorder(); requestPermissions(); + + // 启动视频渲染定时器(10fps) + videoRenderTimer = Timer.periodic(const Duration(milliseconds: 100), (_) { + final int now = DateTime.now().millisecondsSinceEpoch; + if (state.videoBuffer.isNotEmpty) { + final TalkData oldestFrame = state.videoBuffer.removeAt(0); + if (oldestFrame.content.isNotEmpty) { + state.listData.value = Uint8List.fromList(oldestFrame.content); // 备份原始数据 + final int decodeStart = DateTime.now().millisecondsSinceEpoch; + decodeImageFromList(Uint8List.fromList(oldestFrame.content)).then((ui.Image img) { + final int decodeEnd = DateTime.now().millisecondsSinceEpoch; + state.currentImage.value = img; + _renderedFrameCount++; + // 每秒统计一次fps + if (now - _lastFpsPrintTime >= 1000) { + // print('实际渲染fps: $_renderedFrameCount'); + _renderedFrameCount = 0; + _lastFpsPrintTime = now; + } + }).catchError((e) { + print('图片解码失败: $e'); + }); + } + } + // 如果缓冲区为空,不做任何操作,保持上一次内容 + }); } @override @@ -510,7 +419,7 @@ class TalkViewLogic extends BaseGetXController { stopProcessingAudio(); // 清理图片缓存 - _imageCache.clear(); + // _imageCache.clear(); state.oneMinuteTimeTimer?.cancel(); // 取消旧定时器 state.oneMinuteTimeTimer = null; // 取消旧定时器 state.oneMinuteTime.value = 0; @@ -518,6 +427,10 @@ class TalkViewLogic extends BaseGetXController { _streamSubscription?.cancel(); _isListening = false; + // 释放视频渲染定时器 + videoRenderTimer?.cancel(); + videoRenderTimer = null; + super.onClose(); } @@ -526,6 +439,9 @@ class TalkViewLogic extends BaseGetXController { stopProcessingAudio(); // 重置期望数据 StartChartManage().reSetDefaultTalkExpect(); + // 释放视频渲染定时器 + videoRenderTimer?.cancel(); + videoRenderTimer = null; super.dispose(); } diff --git a/lib/talk/starChart/views/talkView/talk_view_page.dart b/lib/talk/starChart/views/talkView/talk_view_page.dart index f9159cee..264e0252 100644 --- a/lib/talk/starChart/views/talkView/talk_view_page.dart +++ b/lib/talk/starChart/views/talkView/talk_view_page.dart @@ -135,16 +135,12 @@ class _TalkViewPageState extends State child: SizedBox.expand( child: RotatedBox( quarterTurns: startChartManage.rotateAngle ~/ 90, - child: Obx( - () => state.currentImage.value != null - ? RawImage( - image: state.currentImage.value, - width: ScreenUtil().scaleWidth, - height: ScreenUtil().scaleHeight, - fit: BoxFit.cover, - filterQuality: FilterQuality.high, - ) - : Container(color: Colors.transparent), + child: RawImage( + image: state.currentImage.value, + width: ScreenUtil().scaleWidth, + height: ScreenUtil().scaleHeight, + fit: BoxFit.cover, + filterQuality: FilterQuality.high, ), ), ), @@ -160,35 +156,33 @@ class _TalkViewPageState extends State style: TextStyle(color: Colors.black, fontSize: 26.sp), )) : Container()), - Obx( - () => state.listData.value.isNotEmpty && - state.oneMinuteTime.value > 0 - ? Positioned( - top: ScreenUtil().statusBarHeight + 75.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), - ), - ], - ); - }, - ), - ) - : Container(), - ), + Obx(() => + state.listData.value.isNotEmpty && state.oneMinuteTime.value > 0 + ? Positioned( + top: ScreenUtil().statusBarHeight + 75.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), + ), + ], + ); + }, + ), + ) + : Container()), Positioned( bottom: 10.w, child: Container( @@ -458,7 +452,6 @@ class _TalkViewPageState extends State // if (state.talkStatus.value == TalkStatus.answeredSuccessfully && // state.listData.value.length > 0) { // logic.udpOpenDoorAction(); - logic.remoteOpenLock(); // } // if (UDPManage().remoteUnlock == 1) { // logic.udpOpenDoorAction(); @@ -466,6 +459,7 @@ class _TalkViewPageState extends State // } else { // logic.showToast('请在锁设置中开启远程开锁'.tr); // } + logic.remoteOpenLock(); }, ) ]); diff --git a/lib/talk/starChart/views/talkView/talk_view_state.dart b/lib/talk/starChart/views/talkView/talk_view_state.dart index aafc8605..02f83efb 100644 --- a/lib/talk/starChart/views/talkView/talk_view_state.dart +++ b/lib/talk/starChart/views/talkView/talk_view_state.dart @@ -90,6 +90,5 @@ class TalkViewState { RxBool isLongPressing = false.obs; // 旋转角度(以弧度为单位) RxBool hasAudioData = false.obs; // 是否有音频数据 RxInt lastAudioTimestamp = 0.obs; // 最后接收到的音频数据的时间戳 - // 添加图片状态变量 - final Rx currentImage = Rx(null); + Rx currentImage = Rx(null); } From b2d2c75b54a6739f69dfe8d6dfabb4dda580fa5b Mon Sep 17 00:00:00 2001 From: liyi Date: Thu, 8 May 2025 13:43:11 +0800 Subject: [PATCH 064/151] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0peerId=E9=95=BF?= =?UTF-8?q?=E5=BA=A6=E8=A1=A5=E9=BD=90=E9=80=BB=E8=BE=91=E4=BD=86=E4=B8=8D?= =?UTF-8?q?=E4=BD=BF=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/talk/starChart/entity/scp_message.dart | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/talk/starChart/entity/scp_message.dart b/lib/talk/starChart/entity/scp_message.dart index fc8a3141..ca5fcef3 100644 --- a/lib/talk/starChart/entity/scp_message.dart +++ b/lib/talk/starChart/entity/scp_message.dart @@ -68,6 +68,18 @@ class ScpMessage { return 'ScpMessage{ProtocolFlag: $ProtocolFlag, MessageType: $MessageType, MessageId: $MessageId, SpTotal: $SpTotal, SpIndex: $SpIndex, FromPeerId: $FromPeerId, ToPeerId: $ToPeerId, PayloadType: $PayloadType, PayloadCRC: $PayloadCRC, PayloadLength: $PayloadLength, Payload: $Payload}'; } + // 辅助函数:定长字符串编码 + List encodeFixedLengthString(String? str, int length) { + final bytes = utf8.encode(str ?? ''); + if (bytes.length > length) { + return bytes.sublist(0, length); + } else if (bytes.length < length) { + return bytes + List.filled(length - bytes.length, 0); + } else { + return bytes; + } + } + String serialize() { final bytes = []; @@ -98,16 +110,19 @@ class ScpMessage { if (SpIndex != null) { bytes.add(SpIndex!); } - - // FromPeerId (字符串,记录长度) + // FromPeerId (字符串,长度固定为44字节) if (FromPeerId != null) { bytes.addAll(utf8.encode(FromPeerId!)); } + // FromPeerId (44字节定长) + // bytes.addAll(encodeFixedLengthString(FromPeerId, 44)); - // ToPeerId (字符串,假设长度固定为32字节) + // ToPeerId (字符串,长度固定为44字节) if (ToPeerId != null) { bytes.addAll(utf8.encode(ToPeerId!)); } + // ToPeerId (44字节定长) + // bytes.addAll(encodeFixedLengthString(ToPeerId, 44)); // PayloadType (2 bytes) if (PayloadType != null) { From 81b1fdd43da152ee2c135183ca01b3113c051f4a Mon Sep 17 00:00:00 2001 From: liyi Date: Thu, 8 May 2025 15:11:08 +0800 Subject: [PATCH 065/151] =?UTF-8?q?fix:=E6=90=9C=E7=B4=A2=E8=93=9D?= =?UTF-8?q?=E7=89=99=E8=AE=BE=E5=A4=87=E5=8C=BA=E5=88=86,sky=E6=94=B9?= =?UTF-8?q?=E4=B8=BA76?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/blue/blue_manage.dart | 38 ++++++++++++++++++++++++-------------- lib/blue/io_type.dart | 2 +- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/lib/blue/blue_manage.dart b/lib/blue/blue_manage.dart index 064fe984..cdc9ae3e 100755 --- a/lib/blue/blue_manage.dart +++ b/lib/blue/blue_manage.dart @@ -263,7 +263,7 @@ class BlueManage { for (final ScanResult scanResult in results) { if (scanResult.advertisementData.serviceUuids.isNotEmpty) { // AppLog.log( - // '扫描到的设备:${scanResult.advertisementData.serviceUuids[0].toString()}'); + // '扫描到的设备:${scanResult.advertisementData.serviceUuids[0].toString()}====${scanResult.advertisementData.advName}'); } else { continue; } @@ -316,20 +316,30 @@ class BlueManage { } /// 判断是否包含指定的uuid - bool _isMatch(List serviceUuids, - {DeviceType deviceType = DeviceType.blue}) { - // 获取设备类型数组 - List deviceTypeList = getDeviceType(deviceType); - - // 检查 serviceUuids 是否包含 deviceTypeList 中的任意一个值 - if (serviceUuids != null && serviceUuids.isNotEmpty) { - return serviceUuids.any((uuid) { - return deviceTypeList - .any((type) => uuid.toLowerCase().contains(type.toLowerCase())); - }); + bool _isMatch(List serviceUuids, {DeviceType deviceType = DeviceType.blue}) { + final List prefixes = getDeviceType(deviceType).map((e) => e.toLowerCase()).toList(); + for (String uuid in serviceUuids) { + final String cleanUuid = uuid.replaceAll('-', '').toLowerCase(); + if (cleanUuid.length == 8) { + // 8位,判断前两位 + for (final prefix in prefixes) { + if (cleanUuid.startsWith(prefix)) { + return true; + } + } + } else if (cleanUuid.length == 32) { + // 128位,判断前8位的第3、第4位 + final String first8 = cleanUuid.substring(0, 8); + if (first8.length >= 4) { + final String thirdAndFourth = first8.substring(2, 4); // 索引2和3 + for (final prefix in prefixes) { + if (thirdAndFourth == prefix) { + return true; + } + } + } + } } - - // 如果 serviceUuids 为空,则返回 false return false; } diff --git a/lib/blue/io_type.dart b/lib/blue/io_type.dart index ee785811..a5f523b4 100755 --- a/lib/blue/io_type.dart +++ b/lib/blue/io_type.dart @@ -9,7 +9,7 @@ List getDeviceType(DeviceType deviceType) { List t = ['758824']; switch (deviceType) { case DeviceType.blue: - t = ['758824', '75']; + t = ['758824', '75', '768824', '76']; break; case DeviceType.gateway: t = ['758825']; From 443b14450b0637fe8b95fb14ac30c789475290a1 Mon Sep 17 00:00:00 2001 From: liyi Date: Thu, 8 May 2025 14:01:03 +0800 Subject: [PATCH 066/151] =?UTF-8?q?fix:=E6=B8=85=E9=99=A4=E8=A7=92?= =?UTF-8?q?=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/mine/message/messageList/messageList_logic.dart | 4 ++++ pubspec.yaml | 2 ++ 2 files changed, 6 insertions(+) diff --git a/lib/mine/message/messageList/messageList_logic.dart b/lib/mine/message/messageList/messageList_logic.dart index 6c119cee..38ca4975 100755 --- a/lib/mine/message/messageList/messageList_logic.dart +++ b/lib/mine/message/messageList/messageList_logic.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:flutter_app_badger/flutter_app_badger.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:star_lock/tools/baseGetXController.dart'; import '../../../network/api_repository.dart'; import '../../../tools/eventBusEventManage.dart'; @@ -46,6 +48,8 @@ class MessageListLogic extends BaseGetXController { if (entity.errorCode!.codeIsSuccessful) { pageNo = 1; messageListDataRequest(); + // 清除角标 + FlutterAppBadger.removeBadge(); } } diff --git a/pubspec.yaml b/pubspec.yaml index 119e3d25..09ba91d5 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -278,6 +278,8 @@ dependencies: provider: ^6.1.2 dio: ^4.0.6 # 网络请求库 video_thumbnail: ^0.5.3 + # 角标管理 + flutter_app_badger: ^1.3.0 From 9b755e29931bf257da47de2a2e0bd2bda7155089 Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 25 Apr 2025 10:21:05 +0800 Subject: [PATCH 067/151] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0=E5=8E=9F?= =?UTF-8?q?=E7=94=9F=E6=8F=92=E4=BB=B6=E8=A7=A3=E7=A0=81=E7=9A=84=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E3=80=81=E5=A2=9E=E5=8A=A0h264=E3=80=81mjpeg=E5=88=87?= =?UTF-8?q?=E6=8D=A2=E7=9A=84debug=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lockDetail/lockDetail_page.dart | 54 +++++++++++++++++++ .../lockDetail/lockDetail_state.dart | 3 ++ 2 files changed, 57 insertions(+) diff --git a/lib/main/lockDetail/lockDetail/lockDetail_page.dart b/lib/main/lockDetail/lockDetail/lockDetail_page.dart index ed3c9b68..fc307e6f 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_page.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_page.dart @@ -507,6 +507,60 @@ class _LockDetailPageState extends State Widget skWidget() { return ListView( children: [ + // Container( + // padding: EdgeInsets.symmetric(vertical: 15, horizontal: 20), + // margin: EdgeInsets.only(top: 10, bottom: 10), + // decoration: BoxDecoration( + // color: Colors.white, + // borderRadius: BorderRadius.circular(10), + // boxShadow: [ + // BoxShadow( + // color: Colors.black.withOpacity(0.05), + // blurRadius: 5, + // offset: Offset(0, 2), + // ), + // ], + // ), + // child: Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // Text('对讲视频模式'.tr, + // style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + // Row( + // children: [ + // Text('mjpeg', + // style: TextStyle( + // fontSize: 14, + // color: !state.useH264Mode.value + // ? AppColors.mainColor + // : Colors.grey)), + // Obx(() => Switch( + // value: state.useH264Mode.value, + // activeColor: AppColors.mainColor, + // onChanged: (value) { + // state.useH264Mode.value = value; + // if (value) { + // // 使用H264模式 + // StartChartManage() + // .sendH264VideoAndG711AudioTalkExpectData(); + // } else { + // // 使用Image模式 + // StartChartManage() + // .sendImageVideoAndG711AudioTalkExpectData(); + // } + // }, + // )), + // Text('H264'.tr, + // style: TextStyle( + // fontSize: 14, + // color: state.useH264Mode.value + // ? AppColors.mainColor + // : Colors.grey)), + // ], + // ), + // ], + // ), + // ), Visibility( visible: (state.keyInfos.value.keyType == XSConstantMacro.keyTypeTime || diff --git a/lib/main/lockDetail/lockDetail/lockDetail_state.dart b/lib/main/lockDetail/lockDetail/lockDetail_state.dart index 44efb77d..a949b693 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_state.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_state.dart @@ -58,6 +58,9 @@ class LockDetailState { int logCountPage = 10; // 蓝牙记录一页多少个 RxInt nextAuthTime = 0.obs; // 下次认证时间 + // 视频编码模式选择开关状态 + RxBool useH264Mode = true.obs; // true表示使用H264模式,false表示使用Image模式 + // LockDetailState() { // Map map = Get.arguments; // lockCount = map["lockCount"]; From d27c4e2f9f0e8246714e0101ffc4894c233f1fd5 Mon Sep 17 00:00:00 2001 From: liyi Date: Sun, 27 Apr 2025 09:53:05 +0800 Subject: [PATCH 068/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E6=97=A0?= =?UTF-8?q?=E9=9F=B3=E9=A2=91=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handle/impl/udp_talk_request_handler.dart | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart index 55a2ac27..615c7316 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart @@ -214,4 +214,27 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle print('app主动发请求,收到回复后发送的预期数据=======锁不支持H264和MJPEG,默认发送图像视频格式期望数据'); } } + + /// app主动发请求,收到回复后发送的预期数据 + void _handleResponseSendExpect() { + final LockListInfoItemEntity currentKeyInfo = + CommonDataManage().currentKeyInfo; + final isH264 = currentKeyInfo.lockFeature?.isH264 == 1; + final isMJpeg = currentKeyInfo.lockFeature?.isMJpeg == 1; + + // 优先使用H264,其次是MJPEG + if (isH264) { + // 锁支持H264,发送H264视频和G711音频期望 + startChartManage.sendH264VideoAndG711AudioTalkExpectData(); + print('锁支持H264,发送H264视频格式期望数据'); + } else if (isMJpeg) { + // 锁只支持MJPEG,发送图像视频和G711音频期望 + startChartManage.sendImageVideoAndG711AudioTalkExpectData(); + print('锁不支持H264,支持MJPEG,发送MJPEG视频格式期望数据'); + } else { + // 默认使用图像视频 + startChartManage.sendImageVideoAndG711AudioTalkExpectData(); + print('锁不支持H264和MJPEG,默认发送图像视频格式期望数据'); + } + } } From 203b625ddda5b0f08a2b0ba945b421386a015c15 Mon Sep 17 00:00:00 2001 From: liyi Date: Mon, 28 Apr 2025 09:21:11 +0800 Subject: [PATCH 069/151] =?UTF-8?q?fix:=E7=A7=BB=E9=99=A4h264=E3=80=81mjpe?= =?UTF-8?q?g=E5=88=87=E6=8D=A2debug=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lockDetail/lockDetail_page.dart | 54 ------------------- .../lockDetail/lockDetail_state.dart | 3 -- 2 files changed, 57 deletions(-) diff --git a/lib/main/lockDetail/lockDetail/lockDetail_page.dart b/lib/main/lockDetail/lockDetail/lockDetail_page.dart index fc307e6f..ed3c9b68 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_page.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_page.dart @@ -507,60 +507,6 @@ class _LockDetailPageState extends State Widget skWidget() { return ListView( children: [ - // Container( - // padding: EdgeInsets.symmetric(vertical: 15, horizontal: 20), - // margin: EdgeInsets.only(top: 10, bottom: 10), - // decoration: BoxDecoration( - // color: Colors.white, - // borderRadius: BorderRadius.circular(10), - // boxShadow: [ - // BoxShadow( - // color: Colors.black.withOpacity(0.05), - // blurRadius: 5, - // offset: Offset(0, 2), - // ), - // ], - // ), - // child: Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // Text('对讲视频模式'.tr, - // style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), - // Row( - // children: [ - // Text('mjpeg', - // style: TextStyle( - // fontSize: 14, - // color: !state.useH264Mode.value - // ? AppColors.mainColor - // : Colors.grey)), - // Obx(() => Switch( - // value: state.useH264Mode.value, - // activeColor: AppColors.mainColor, - // onChanged: (value) { - // state.useH264Mode.value = value; - // if (value) { - // // 使用H264模式 - // StartChartManage() - // .sendH264VideoAndG711AudioTalkExpectData(); - // } else { - // // 使用Image模式 - // StartChartManage() - // .sendImageVideoAndG711AudioTalkExpectData(); - // } - // }, - // )), - // Text('H264'.tr, - // style: TextStyle( - // fontSize: 14, - // color: state.useH264Mode.value - // ? AppColors.mainColor - // : Colors.grey)), - // ], - // ), - // ], - // ), - // ), Visibility( visible: (state.keyInfos.value.keyType == XSConstantMacro.keyTypeTime || diff --git a/lib/main/lockDetail/lockDetail/lockDetail_state.dart b/lib/main/lockDetail/lockDetail/lockDetail_state.dart index a949b693..44efb77d 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_state.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_state.dart @@ -58,9 +58,6 @@ class LockDetailState { int logCountPage = 10; // 蓝牙记录一页多少个 RxInt nextAuthTime = 0.obs; // 下次认证时间 - // 视频编码模式选择开关状态 - RxBool useH264Mode = true.obs; // true表示使用H264模式,false表示使用Image模式 - // LockDetailState() { // Map map = Get.arguments; // lockCount = map["lockCount"]; From 47f5096a9c11defe274100264adcce4555e4f05d Mon Sep 17 00:00:00 2001 From: liyi Date: Wed, 30 Apr 2025 17:55:57 +0800 Subject: [PATCH 070/151] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0native=E8=A7=A3?= =?UTF-8?q?=E7=A0=81=E6=8F=92=E4=BB=B6=E6=94=AF=E6=8C=81720P=E5=AF=B9?= =?UTF-8?q?=E8=AE=B2=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/talk/starChart/constant/talk_constant.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/talk/starChart/constant/talk_constant.dart b/lib/talk/starChart/constant/talk_constant.dart index ff8a6727..a2c95122 100644 --- a/lib/talk/starChart/constant/talk_constant.dart +++ b/lib/talk/starChart/constant/talk_constant.dart @@ -13,7 +13,11 @@ class TalkConstant { audioType: [AudioTypeE.G711], ); static TalkExpectReq H264Expect = TalkExpectReq( - videoType: [VideoTypeE.H264], + videoType: [VideoTypeE.H264_720P], + audioType: [AudioTypeE.G711], + ); + static TalkExpectReq H264_720P_Expect = TalkExpectReq( + videoType: [VideoTypeE.H264_720P], audioType: [AudioTypeE.G711], ); static TalkExpectReq H264_720P_Expect = TalkExpectReq( From 7f7bb49d8c5b427f769a63befe04022242725d58 Mon Sep 17 00:00:00 2001 From: liyi Date: Tue, 6 May 2025 11:42:16 +0800 Subject: [PATCH 071/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E4=B8=BA?= =?UTF-8?q?=E5=8E=9F=E6=9C=89=E7=9A=84864*480=E5=AF=B9=E8=AE=B2=E8=A7=86?= =?UTF-8?q?=E9=A2=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/talk/starChart/constant/talk_constant.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/talk/starChart/constant/talk_constant.dart b/lib/talk/starChart/constant/talk_constant.dart index a2c95122..fd5092c9 100644 --- a/lib/talk/starChart/constant/talk_constant.dart +++ b/lib/talk/starChart/constant/talk_constant.dart @@ -13,7 +13,7 @@ class TalkConstant { audioType: [AudioTypeE.G711], ); static TalkExpectReq H264Expect = TalkExpectReq( - videoType: [VideoTypeE.H264_720P], + videoType: [VideoTypeE.H264], audioType: [AudioTypeE.G711], ); static TalkExpectReq H264_720P_Expect = TalkExpectReq( From 79f2d95314c4070a96845bbfea82796bdbfe80c2 Mon Sep 17 00:00:00 2001 From: liyi Date: Thu, 8 May 2025 15:37:10 +0800 Subject: [PATCH 072/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E7=82=B9?= =?UTF-8?q?=E5=87=BB=E9=9D=99=E9=9F=B3=E6=97=B6=E5=8F=91=E9=80=81=E6=9C=9F?= =?UTF-8?q?=E6=9C=9B=E6=95=B0=E6=8D=AE=E8=A2=AB=E6=94=B9=E4=B8=BAiamge?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../starChart/constant/talk_constant.dart | 4 ---- .../handle/impl/udp_talk_request_handler.dart | 23 ------------------- .../native/talk_view_native_decode_logic.dart | 4 ++-- 3 files changed, 2 insertions(+), 29 deletions(-) diff --git a/lib/talk/starChart/constant/talk_constant.dart b/lib/talk/starChart/constant/talk_constant.dart index fd5092c9..ff8a6727 100644 --- a/lib/talk/starChart/constant/talk_constant.dart +++ b/lib/talk/starChart/constant/talk_constant.dart @@ -20,8 +20,4 @@ class TalkConstant { videoType: [VideoTypeE.H264_720P], audioType: [AudioTypeE.G711], ); - static TalkExpectReq H264_720P_Expect = TalkExpectReq( - videoType: [VideoTypeE.H264_720P], - audioType: [AudioTypeE.G711], - ); } diff --git a/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart index 615c7316..55a2ac27 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart @@ -214,27 +214,4 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle print('app主动发请求,收到回复后发送的预期数据=======锁不支持H264和MJPEG,默认发送图像视频格式期望数据'); } } - - /// app主动发请求,收到回复后发送的预期数据 - void _handleResponseSendExpect() { - final LockListInfoItemEntity currentKeyInfo = - CommonDataManage().currentKeyInfo; - final isH264 = currentKeyInfo.lockFeature?.isH264 == 1; - final isMJpeg = currentKeyInfo.lockFeature?.isMJpeg == 1; - - // 优先使用H264,其次是MJPEG - if (isH264) { - // 锁支持H264,发送H264视频和G711音频期望 - startChartManage.sendH264VideoAndG711AudioTalkExpectData(); - print('锁支持H264,发送H264视频格式期望数据'); - } else if (isMJpeg) { - // 锁只支持MJPEG,发送图像视频和G711音频期望 - startChartManage.sendImageVideoAndG711AudioTalkExpectData(); - print('锁不支持H264,支持MJPEG,发送MJPEG视频格式期望数据'); - } else { - // 默认使用图像视频 - startChartManage.sendImageVideoAndG711AudioTalkExpectData(); - print('锁不支持H264和MJPEG,默认发送图像视频格式期望数据'); - } - } } diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart index f416f98b..033c3f57 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart @@ -519,13 +519,13 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { state.isOpenVoice.value = !state.isOpenVoice.value; if (!state.isOpenVoice.value) { talkExpectReq = TalkExpectReq( - videoType: [VideoTypeE.IMAGE], + videoType: [VideoTypeE.H264], audioType: [], ); showToast('已静音'.tr); } else { talkExpectReq = TalkExpectReq( - videoType: [VideoTypeE.IMAGE], + videoType: [VideoTypeE.H264], audioType: [AudioTypeE.G711], ); } From 29fc822b9173d55f89d040356e7cbd1985ae81aa Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 9 May 2025 09:30:40 +0800 Subject: [PATCH 073/151] =?UTF-8?q?fix:=E6=A0=B9=E6=8D=AE=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E5=88=A4=E6=96=AD=E5=BC=80=E5=A7=8B=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E9=94=81=E6=9D=BF=E7=9A=84=E6=8C=87=E7=BA=B9?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E6=95=B0=E6=8D=AE=E6=97=B6=E9=9C=80=E8=A6=81?= =?UTF-8?q?=E6=94=B9=E4=B8=BA255?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../addFingerprint/addFingerprint_logic.dart | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/main/lockDetail/fingerprint/addFingerprint/addFingerprint_logic.dart b/lib/main/lockDetail/fingerprint/addFingerprint/addFingerprint_logic.dart index c5ff8d1c..ebf94158 100755 --- a/lib/main/lockDetail/fingerprint/addFingerprint/addFingerprint_logic.dart +++ b/lib/main/lockDetail/fingerprint/addFingerprint/addFingerprint_logic.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:get/get.dart'; import 'package:star_lock/apm/apm_helper.dart'; +import 'package:star_lock/flavors.dart'; import 'package:star_lock/login/login/entity/LoginEntity.dart'; import 'package:star_lock/main/lockDetail/fingerprint/addFingerprint/addFingerprint_entity.dart'; import 'package:star_lock/tools/dateTool.dart'; @@ -141,7 +142,7 @@ class AddFingerprintLogic extends BaseGetXController { break; case 0xFE: case 12: - // 管理员已满 + // 管理员已满 state.ifAddState.value = false; showToast('管理员已满'.tr, something: () { Get.back(); @@ -250,6 +251,12 @@ class AddFingerprintLogic extends BaseGetXController { final List? token = await Storage.getStringList(saveBlueToken); final List getTokenList = changeStringListToIntList(token!); + String startTime = DateTool().dateToHNString(state.effectiveDateTime.value); + String endTime = DateTool().dateToHNString(state.failureDateTime.value); + if (F.isSKY) { + startTime = '255:00'; + endTime = '255:00'; + } final String command = SenderAddFingerprintWithTimeCycleCoercionCommand( keyID: '1', @@ -267,8 +274,8 @@ class AddFingerprintLogic extends BaseGetXController { // 周循环 startDate: int.parse(state.startDate.value) ~/ 1000, endDate: int.parse(state.endDate.value) ~/ 1000, - startTime: DateTool().dateToHNString(state.effectiveDateTime.value), - endTime: DateTool().dateToHNString(state.failureDateTime.value), + startTime: startTime, + endTime: endTime, needAuthor: 1, signKey: signKeyDataList, privateKey: getPrivateKeyList, @@ -324,8 +331,8 @@ class AddFingerprintLogic extends BaseGetXController { // 周循环 startDate: int.parse(state.startDate.value) ~/ 1000, endDate: int.parse(state.endDate.value) ~/ 1000, - startTime: DateTool().dateToHNString(state.effectiveDateTime.value), - endTime: DateTool().dateToHNString(state.failureDateTime.value), + startTime: startTime, + endTime: endTime, needAuthor: 1, signKey: signKeyDataList, privateKey: getPrivateKeyList, From 2cdcd691089b13fac5b70a2265c98066f466e166 Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 9 May 2025 15:47:41 +0800 Subject: [PATCH 074/151] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0sky=E7=9A=84ci?= =?UTF-8?q?=E5=88=86=E6=94=AF=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index eeb98d56..c088932c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -18,6 +18,7 @@ variables: rules: - if: $CI_COMMIT_BRANCH == "develop" - if: $CI_COMMIT_BRANCH == "release" + - if: $CI_COMMIT_BRANCH == "release_sky" - if: $CI_COMMIT_BRANCH =~ /feat_[a-zA-Z]+/ - if: $CI_COMMIT_BRANCH == "canary_release" - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]+\.[0-9]+)?$/ @@ -29,6 +30,7 @@ variables: rules: - if: $CI_COMMIT_BRANCH == "develop" - if: $CI_COMMIT_BRANCH == "release" + - if: $CI_COMMIT_BRANCH == "release_sky" - if: $CI_COMMIT_BRANCH =~ /feat_[a-zA-Z]+/ .generate_tag_rule: @@ -45,6 +47,7 @@ variables: rules: - if: $CI_COMMIT_BRANCH == "develop" - if: $CI_COMMIT_BRANCH == "release" + - if: $CI_COMMIT_BRANCH == "release_sky" - if: $CI_COMMIT_BRANCH == "canary_release" - if: $CI_COMMIT_BRANCH =~ /feat_[a-zA-Z]+/ From eed97a482543fec838adec4499e7c7abb7bcab48 Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 9 May 2025 15:59:45 +0800 Subject: [PATCH 075/151] =?UTF-8?q?fix:=E6=B5=8B=E8=AF=95ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 45dfc905..d6ee8c02 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # 星锁APP - +测试ci 星云项目组旗下的智能锁应用,其中锁相关数据接入星云平台,业务数据接入星锁自有后台。 基于Flutter技术架构,支持Android和iOS平台。 From 869beca8f194a6ea33726ea5991c4727ccfd2634 Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 9 May 2025 16:16:06 +0800 Subject: [PATCH 076/151] =?UTF-8?q?fix:=E6=B5=8B=E8=AF=95ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c088932c..17a01616 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -39,6 +39,7 @@ variables: - flutter rules: - if: $CI_COMMIT_BRANCH == "master" + - if: $CI_COMMIT_BRANCH == "master_sky" .generate_next_version_rule: tags: From 08d41a6925beb96b82c5ccf4bccbd5c121e9ecdf Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 9 May 2025 16:23:57 +0800 Subject: [PATCH 077/151] =?UTF-8?q?fix:=E6=B5=8B=E8=AF=95ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/build.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/android/build.sh b/android/build.sh index 4b598959..f444cb1e 100755 --- a/android/build.sh +++ b/android/build.sh @@ -28,5 +28,9 @@ elif [[ "${ENV_BUILD_BRANCH}" == "release" ]] || [[ "${ENV_BUILD_BRANCH}" == "fe echo "===build pre===${NEXT_VERSION}" bundle exec fastlane beta flavor:xhj env:pre --verbose bundle exec fastlane beta flavor:sky env:pre --verbose +elif [[ "${ENV_BUILD_BRANCH}" == "release_sky" ]]; then + echo "===build release_sky===${NEXT_VERSION}" + bundle exec fastlane release_apk flavor:sky --verbose + bundle exec fastlane release_bundle flavor:sky --verbose fi exit 0 \ No newline at end of file From bbfaec71c83597aca9fecd7494533f6fbf911c01 Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 9 May 2025 16:44:21 +0800 Subject: [PATCH 078/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4sky=E4=B8=8B?= =?UTF-8?q?=E7=9A=84ci=E4=B8=BAmaster=5Fsky=E8=BF=9B=E8=A1=8C=E7=94=9F?= =?UTF-8?q?=E6=88=90tag=E5=92=8Cversion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tag_generator.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tag_generator.sh b/tag_generator.sh index 27e39182..b5c16ca8 100755 --- a/tag_generator.sh +++ b/tag_generator.sh @@ -19,11 +19,11 @@ else major="${major#v}" compare_json="" if [[ "$1" == "generate_tag" ]];then - echo "generate_tag:$newest_tag-to-master\n" - compare_json=$(curl -s --header "PRIVATE-TOKEN: $TOKEN" "$URL/projects/$PROJECT_ID/repository/compare?from=$newest_tag&to=master") + echo "generate_tag:$newest_tag-to-master_sky\n" + compare_json=$(curl -s --header "PRIVATE-TOKEN: $TOKEN" "$URL/projects/$PROJECT_ID/repository/compare?from=$newest_tag&to=master_sky") elif [[ "$1" == "generate_version" ]]; then - echo "generate_version:master-to-$CI_COMMIT_BRANCH\n" - compare_json=$(curl -s --header "PRIVATE-TOKEN: $TOKEN" "$URL/projects/$PROJECT_ID/repository/compare?from=master&to=$CI_COMMIT_BRANCH") + echo "generate_version:master_sky-to-$CI_COMMIT_BRANCH\n" + compare_json=$(curl -s --header "PRIVATE-TOKEN: $TOKEN" "$URL/projects/$PROJECT_ID/repository/compare?from=master_sky&to=$CI_COMMIT_BRANCH") fi echo "compare_json:$compare_json\n" new_patch=$patch From 9939685c941d7918d82c4e097c87ea15f5c76b2b Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 9 May 2025 16:51:18 +0800 Subject: [PATCH 079/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4sky=E4=B8=8B?= =?UTF-8?q?=E7=9A=84ci=E4=B8=BAmaster=5Fsky=E8=BF=9B=E8=A1=8C=E7=94=9F?= =?UTF-8?q?=E6=88=90tag=E6=97=B6=E5=A2=9E=E5=8A=A0=E5=89=8D=E7=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tag_generator.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tag_generator.sh b/tag_generator.sh index b5c16ca8..f83b2c54 100755 --- a/tag_generator.sh +++ b/tag_generator.sh @@ -45,6 +45,13 @@ else done < <(echo "$compare_json" | jq -c '.commits[] | {id: .id, message: .message}') next_tag="v$major.$new_minor.$new_patch" fi + +# 生成新tag名时,若在master_sky分支,加前缀sky_ +branch_name="$CI_COMMIT_BRANCH" +if [[ "$branch_name" == "master_sky" ]]; then + next_tag="sky_${next_tag}" +fi + echo "New Tag:$newest_tag;New version: $next_tag;command: $1" if [[ "$1" == "generate_tag" ]];then if [ "$next_tag" == "$newest_tag" ]; then From d3080bba7d79da7a9bdf7ca46c57b1d272c9058e Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 9 May 2025 17:03:55 +0800 Subject: [PATCH 080/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4sky=E4=B8=8B?= =?UTF-8?q?=E7=9A=84ci=E4=B8=BAmaster=5Fsky=E8=BF=9B=E8=A1=8C=E7=94=9F?= =?UTF-8?q?=E6=88=90tag=E6=97=B6=E5=A2=9E=E5=8A=A0=E5=89=8D=E7=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/fastlane/Fastfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/fastlane/Fastfile b/android/fastlane/Fastfile index a99f62f0..2c66fff4 100644 --- a/android/fastlane/Fastfile +++ b/android/fastlane/Fastfile @@ -90,7 +90,7 @@ platform :android do print_log "build flavor for: #{flavor}" build_number = Time.now.strftime("%Y%m%d%H") print_log "BuildNo #{build_number}" - build_version = $current_tag.match(/^v(\d+\.\d+\.\d+)/).captures[0] + build_version = $current_tag.match(/^(sky_)?v(\d+\.\d+\.\d+)/).captures.last print_log "buildVersion #{build_version}" commit_hash = last_git_commit short_hash = commit_hash[:abbreviated_commit_hash] @@ -112,7 +112,7 @@ platform :android do print_log "build flavor for: #{flavor}" build_number = Time.now.strftime("%Y%m%d%H") print_log "BuildNo #{build_number}" - build_version = $current_tag.match(/^v(\d+\.\d+\.\d+)/).captures[0] + build_version = $current_tag.match(/^(sky_)?v(\d+\.\d+\.\d+)/).captures.last print_log "buildVersion #{build_version}" commit_hash = last_git_commit short_hash = commit_hash[:abbreviated_commit_hash] From 10a67ddf93bbe12045781cb77d1daae8a64b9a2e Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 9 May 2025 17:11:06 +0800 Subject: [PATCH 081/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4sky=E4=B8=8B?= =?UTF-8?q?=E7=9A=84ci=E4=B8=BAmaster=5Fsky=E8=BF=9B=E8=A1=8C=E7=94=9F?= =?UTF-8?q?=E6=88=90tag=E6=97=B6=E5=A2=9E=E5=8A=A0=E5=89=8D=E7=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tag_generator.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tag_generator.sh b/tag_generator.sh index f83b2c54..35529ba2 100755 --- a/tag_generator.sh +++ b/tag_generator.sh @@ -15,7 +15,9 @@ if [ "$tags_length" -lt 1 ]; then next_tag="v1.0.0" else newest_tag=$(echo "$tags" | head -n 1) - IFS='.' read -r major minor patch <<< "$newest_tag" + # 去除已有的sky_前缀,防止重复 + base_tag=${newest_tag#sky_} + IFS='.' read -r major minor patch <<< "$base_tag" major="${major#v}" compare_json="" if [[ "$1" == "generate_tag" ]];then From 0d8e818f4d83d22b50657b428364569dc3a81d3e Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 9 May 2025 17:34:18 +0800 Subject: [PATCH 082/151] =?UTF-8?q?fix:=E6=B5=8B=E8=AF=95ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d6ee8c02..45dfc905 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # 星锁APP -测试ci + 星云项目组旗下的智能锁应用,其中锁相关数据接入星云平台,业务数据接入星锁自有后台。 基于Flutter技术架构,支持Android和iOS平台。 From 9ac54cc9710f3f9eb8fa487b84799de9cf41d93d Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 9 May 2025 17:36:13 +0800 Subject: [PATCH 083/151] =?UTF-8?q?fix:=E6=B5=8B=E8=AF=95ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 45dfc905..85c78d24 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # 星锁APP - +fix:测试ci 星云项目组旗下的智能锁应用,其中锁相关数据接入星云平台,业务数据接入星锁自有后台。 基于Flutter技术架构,支持Android和iOS平台。 From b99a4a99d44f99dbcc928a6dcf80b1986eb735ab Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 9 May 2025 17:52:58 +0800 Subject: [PATCH 084/151] =?UTF-8?q?fix:=E6=B5=8B=E8=AF=95ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 85c78d24..45dfc905 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # 星锁APP -fix:测试ci + 星云项目组旗下的智能锁应用,其中锁相关数据接入星云平台,业务数据接入星锁自有后台。 基于Flutter技术架构,支持Android和iOS平台。 From c7cc02c045e947e0c2b32d4adcc36d7ca6dd9ab9 Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 9 May 2025 18:21:04 +0800 Subject: [PATCH 085/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E6=B5=81?= =?UTF-8?q?=E6=B0=B4=E7=BA=BFtag=E5=88=A4=E6=96=AD=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 17a01616..b884f3a1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -21,7 +21,7 @@ variables: - if: $CI_COMMIT_BRANCH == "release_sky" - if: $CI_COMMIT_BRANCH =~ /feat_[a-zA-Z]+/ - if: $CI_COMMIT_BRANCH == "canary_release" - - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]+\.[0-9]+)?$/ + - if: $CI_COMMIT_TAG =~ /^(sky_)?v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]+\.[0-9]+)?$/ .notify_rule: tags: From 2f9328bd335889eb511929dc9906147cd61bed12 Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 9 May 2025 18:28:32 +0800 Subject: [PATCH 086/151] =?UTF-8?q?fix:=E6=B5=8B=E8=AF=95ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 45dfc905..85c78d24 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # 星锁APP - +fix:测试ci 星云项目组旗下的智能锁应用,其中锁相关数据接入星云平台,业务数据接入星锁自有后台。 基于Flutter技术架构,支持Android和iOS平台。 From cf2fd775ec33240eb99860739eb5b8d08138ba63 Mon Sep 17 00:00:00 2001 From: liyi Date: Mon, 12 May 2025 09:59:48 +0800 Subject: [PATCH 087/151] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E9=94=81=E4=B9=8B=E5=90=8E=E4=B8=8D=E5=87=BA=E7=8E=B0?= =?UTF-8?q?=E5=9C=A8=E5=88=97=E8=A1=A8=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuringWifi_logic.dart | 1 + .../lockMian/lockList/lockList_logic.dart | 13 ++++++++- .../lockMian/lockMain/lockMain_logic.dart | 29 ++++++++++--------- lib/tools/eventBusEventManage.dart | 9 ++++++ 4 files changed, 37 insertions(+), 15 deletions(-) diff --git a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart index 76cc2bf4..7b266390 100755 --- a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart +++ b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart @@ -189,6 +189,7 @@ class ConfiguringWifiLogic extends BaseGetXController { Get.offAllNamed(Routers.starLockMain); } eventBus.fire(SuccessfulDistributionNetwork()); + eventBus.fire(RefreshLockListInfoDataEvent()); }); // 获取锁设置 diff --git a/lib/main/lockMian/lockList/lockList_logic.dart b/lib/main/lockMian/lockList/lockList_logic.dart index 4ecd4e62..76a9bb2b 100755 --- a/lib/main/lockMian/lockList/lockList_logic.dart +++ b/lib/main/lockMian/lockList/lockList_logic.dart @@ -61,7 +61,7 @@ class LockListLogic extends BaseGetXController { void setLockListInfoGroupEntity(LockListInfoGroupEntity entity) { this.entity = entity; // if (entity.pageNo == 1) { - _groupDataList = []; + _groupDataList = []; // } _groupDataList.addAll(entity.groupList!); update(); @@ -69,6 +69,7 @@ class LockListLogic extends BaseGetXController { // 监听蓝牙协议返回结果 late StreamSubscription _replySubscription; + late StreamSubscription _setLockListInfoGroupEntity; void _initReplySubscription() { _replySubscription = @@ -336,6 +337,7 @@ class LockListLogic extends BaseGetXController { void onReady() { super.onReady(); _initReplySubscription(); + _initEventHandler(); } @override @@ -348,5 +350,14 @@ class LockListLogic extends BaseGetXController { @override void onClose() { _replySubscription.cancel(); + _setLockListInfoGroupEntity.cancel(); + } + + void _initEventHandler() { + _setLockListInfoGroupEntity = eventBus + .on() + .listen((SetLockListInfoGroupEntity event) async { + setLockListInfoGroupEntity(event.lockListInfoGroupEntity); + }); } } diff --git a/lib/main/lockMian/lockMain/lockMain_logic.dart b/lib/main/lockMian/lockMain/lockMain_logic.dart index 802e9bbe..3ac67134 100755 --- a/lib/main/lockMian/lockMain/lockMain_logic.dart +++ b/lib/main/lockMian/lockMain/lockMain_logic.dart @@ -132,20 +132,21 @@ class LockMainLogic extends BaseGetXController { state.lockListInfoGroupEntity.refresh(); // AppLog.log('entity:$entity state.lockListInfoGroupEntity.value.groupList!.length:${state.lockListInfoGroupEntity.value.groupList![0].lockList!.length}'); //检测控制器是否存在 - if (Get.isRegistered()) { - //设置控制器数据并刷新 - // AppLog.log('检测控制器是否存 调用了 setLockListInfoGroupEntity'); - Get.find().setLockListInfoGroupEntity(entity); - } else { - //延迟加载 - Future.delayed(200.milliseconds, () { - if (Get.isRegistered()) { - //设置控制器数据并刷新 - // AppLog.log('检测控制器是否存 延迟调用了 setLockListInfoGroupEntity'); - Get.find().setLockListInfoGroupEntity(entity); - } - }); - } + eventBus.fire(SetLockListInfoGroupEntity(lockListInfoGroupEntity: entity)); + // if (Get.isRegistered()) { + // //设置控制器数据并刷新 + // // AppLog.log('检测控制器是否存 调用了 setLockListInfoGroupEntity'); + // Get.find().setLockListInfoGroupEntity(entity); + // } else { + // //延迟加载 + // Future.delayed(500.milliseconds, () { + // if (Get.isRegistered()) { + // //设置控制器数据并刷新 + // // AppLog.log('检测控制器是否存 延迟调用了 setLockListInfoGroupEntity'); + // Get.find().setLockListInfoGroupEntity(entity); + // } + // }); + // } if (state.dataLength.value == 1) { if (Get.isRegistered()) { diff --git a/lib/tools/eventBusEventManage.dart b/lib/tools/eventBusEventManage.dart index 810ad0a6..4c4fbb3d 100755 --- a/lib/tools/eventBusEventManage.dart +++ b/lib/tools/eventBusEventManage.dart @@ -1,4 +1,5 @@ import 'package:event_bus/event_bus.dart'; +import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart'; import '../main/lockDetail/lockSet/lockSet/lockSetInfo_entity.dart'; @@ -195,6 +196,7 @@ class RogerThatLockInfoDataEvent { class GetGatewayListRefreshUI { GetGatewayListRefreshUI(); } + /// 同意隐私协议 class AgreePrivacyAgreement { AgreePrivacyAgreement(); @@ -204,3 +206,10 @@ class AgreePrivacyAgreement { class SuccessfulDistributionNetwork { SuccessfulDistributionNetwork(); } + +/// 设置锁列表数据 +class SetLockListInfoGroupEntity { + SetLockListInfoGroupEntity({required this.lockListInfoGroupEntity}); + + LockListInfoGroupEntity lockListInfoGroupEntity; +} From 310062513bd0c67c6688932fe71c62eb55daf04c Mon Sep 17 00:00:00 2001 From: liyi Date: Mon, 12 May 2025 13:57:10 +0800 Subject: [PATCH 088/151] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E9=94=81=E4=B9=8B=E5=90=8E=E4=B8=8D=E5=87=BA=E7=8E=B0?= =?UTF-8?q?=E5=9C=A8=E5=88=97=E8=A1=A8=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuringWifi_logic.dart | 2 +- .../lockMian/lockList/lockList_logic.dart | 19 +++++----- lib/main/lockMian/lockList/lockList_page.dart | 37 +++++++++---------- 3 files changed, 28 insertions(+), 30 deletions(-) diff --git a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart index 7b266390..6f0a4570 100755 --- a/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart +++ b/lib/main/lockDetail/lockSet/configuringWifi/configuringWifi/configuringWifi_logic.dart @@ -189,7 +189,7 @@ class ConfiguringWifiLogic extends BaseGetXController { Get.offAllNamed(Routers.starLockMain); } eventBus.fire(SuccessfulDistributionNetwork()); - eventBus.fire(RefreshLockListInfoDataEvent()); + eventBus.fire(RefreshLockListInfoDataEvent(clearScanDevices: true,isUnShowLoading: true)); }); // 获取锁设置 diff --git a/lib/main/lockMian/lockList/lockList_logic.dart b/lib/main/lockMian/lockList/lockList_logic.dart index 76a9bb2b..948546d3 100755 --- a/lib/main/lockMian/lockList/lockList_logic.dart +++ b/lib/main/lockMian/lockList/lockList_logic.dart @@ -27,13 +27,13 @@ class LockListLogic extends BaseGetXController { LockListLogic(this.entity) {} LockListState state = LockListState(); - List _groupDataList = []; + final RxList groupDataList = [].obs; LockListInfoGroupEntity? entity; final ShowTipView showTipView = ShowTipView(); - List get groupDataList { + List get groupDataListFiltered { final List list = - _groupDataList.map((GroupList e) => e.copy()).toList(); + groupDataList.map((GroupList e) => e.copy()).toList(); if (state.searchStr.value != '' && state.showSearch.value) { list.forEach((GroupList element) { element.lockList?.removeWhere((LockListInfoItemEntity element) => @@ -60,11 +60,7 @@ class LockListLogic extends BaseGetXController { //设置数据 void setLockListInfoGroupEntity(LockListInfoGroupEntity entity) { this.entity = entity; - // if (entity.pageNo == 1) { - _groupDataList = []; - // } - _groupDataList.addAll(entity.groupList!); - update(); + groupDataList.value = entity.groupList!; } // 监听蓝牙协议返回结果 @@ -343,8 +339,11 @@ class LockListLogic extends BaseGetXController { @override void onInit() { super.onInit(); - // AppLog.log('onInit调用了 setLockListInfoGroupEntity'); - setLockListInfoGroupEntity(entity!); + AppLog.log('[onInit] entity: \\${entity?.toString()}'); + if (entity != null) { + setLockListInfoGroupEntity(entity!); + } + _initEventHandler(); } @override diff --git a/lib/main/lockMian/lockList/lockList_page.dart b/lib/main/lockMian/lockList/lockList_page.dart index 1e53cdb9..55cb149a 100755 --- a/lib/main/lockMian/lockList/lockList_page.dart +++ b/lib/main/lockMian/lockList/lockList_page.dart @@ -37,32 +37,31 @@ class _LockListPageState extends State with RouteAware { @override Widget build(BuildContext context) { - return GetBuilder(builder: (LockListLogic logic) { - return Scaffold( - body: ListView.separated( - itemCount: logic.groupDataList.length, - itemBuilder: (BuildContext context, int index) { - final GroupList itemData = logic.groupDataList[index]; - return _buildLockExpandedList(context, index, itemData); - }, - shrinkWrap: true, - physics: const AlwaysScrollableScrollPhysics(), - separatorBuilder: (BuildContext context, int index) { - return const Divider( - height: 1, - color: AppColors.greyLineColor, - ); - }), - ); - }); + return Obx(() => Scaffold( + body: ListView.separated( + itemCount: logic.groupDataListFiltered.length, + itemBuilder: (BuildContext context, int index) { + final GroupList itemData = logic.groupDataListFiltered[index]; + return _buildLockExpandedList(context, index, itemData, key: ValueKey(itemData.groupId)); + }, + shrinkWrap: true, + physics: const AlwaysScrollableScrollPhysics(), + separatorBuilder: (BuildContext context, int index) { + return const Divider( + height: 1, + color: AppColors.greyLineColor, + ); + }), + )); } //设备多层级列表 Widget _buildLockExpandedList(BuildContext context, int index, - GroupList itemData) { + GroupList itemData, {Key? key}) { final List lockItemList = itemData.lockList ?? []; return LockListGroupView( + key: key, onTap: () { //是否选中组 if (itemData.isChecked) {} else {} From 4a725be23fa953e970023aa0d377bef79ee4b8a2 Mon Sep 17 00:00:00 2001 From: liyi Date: Mon, 12 May 2025 13:57:50 +0800 Subject: [PATCH 089/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E8=93=9D?= =?UTF-8?q?=E7=89=99=E6=90=9C=E7=B4=A2=E6=97=B6=E5=88=A4=E6=96=AD=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E5=88=B0=E7=9A=84=E8=AE=BE=E5=A4=87=E6=98=AF=E5=90=A6?= =?UTF-8?q?=E9=85=8D=E5=AF=B9=E7=9A=84=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/blue/blue_manage.dart | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/lib/blue/blue_manage.dart b/lib/blue/blue_manage.dart index cdc9ae3e..0259a5c0 100755 --- a/lib/blue/blue_manage.dart +++ b/lib/blue/blue_manage.dart @@ -319,22 +319,34 @@ class BlueManage { bool _isMatch(List serviceUuids, {DeviceType deviceType = DeviceType.blue}) { final List prefixes = getDeviceType(deviceType).map((e) => e.toLowerCase()).toList(); for (String uuid in serviceUuids) { - final String cleanUuid = uuid.replaceAll('-', '').toLowerCase(); + final String cleanUuid = uuid.toLowerCase(); if (cleanUuid.length == 8) { - // 8位,判断前两位 + // 8位,判断第4、5位 + String pairStatus = cleanUuid.substring(4, 6); // 第4、5位(索引3和4) for (final prefix in prefixes) { if (cleanUuid.startsWith(prefix)) { - return true; + // 00=未配对,01=已配对 + if (pairStatus == '00') { + return true; // 未配对才返回true + } + // 已配对(01)不返回true,继续判断下一个uuid } } - } else if (cleanUuid.length == 32) { + } else { // 128位,判断前8位的第3、第4位 - final String first8 = cleanUuid.substring(0, 8); - if (first8.length >= 4) { - final String thirdAndFourth = first8.substring(2, 4); // 索引2和3 + if (cleanUuid.length >= 32) { + final String thirdAndFourth = cleanUuid.substring(2, 4); // 索引2和3 for (final prefix in prefixes) { if (thirdAndFourth == prefix) { - return true; + // 判断配对状态(带横杠UUID的第31、32位,从1开始计数) + if (cleanUuid.length >= 32) { + String pairStatus = cleanUuid.substring(30, 32); // 第31、32位(从1开始计数) + // 00=未配对,01=已配对 + if (pairStatus == '00') { + return true; // 未配对才返回true + } + // 已配对(01)不返回true,继续判断下一个uuid + } } } } From 905368ec8da405630e54690931ac13988c805e13 Mon Sep 17 00:00:00 2001 From: liyi Date: Tue, 13 May 2025 09:45:57 +0800 Subject: [PATCH 090/151] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0=E5=9B=BE?= =?UTF-8?q?=E4=BC=A0=E5=85=A8=E8=87=AA=E5=8A=A8=E9=94=81=E9=80=9A=E8=AF=9D?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/appRouters.dart | 11 +- .../image_transmission_logic.dart | 667 ++++++++++++++++++ .../image_transmission_page.dart | 238 +++++++ .../image_transmission_state.dart | 94 +++ 4 files changed, 1009 insertions(+), 1 deletion(-) create mode 100644 lib/talk/starChart/views/imageTransmission/image_transmission_logic.dart create mode 100644 lib/talk/starChart/views/imageTransmission/image_transmission_page.dart create mode 100644 lib/talk/starChart/views/imageTransmission/image_transmission_state.dart diff --git a/lib/appRouters.dart b/lib/appRouters.dart index 9043ffe1..0a06445a 100755 --- a/lib/appRouters.dart +++ b/lib/appRouters.dart @@ -203,6 +203,7 @@ import 'mine/valueAddedServices/valueAddedServicesRealName/value_added_services_ import 'mine/valueAddedServices/valueAddedServicesSMSTemplate/valueAddedServicesAddSMSTemplate/newSMSTemplate_page.dart'; import 'mine/valueAddedServices/valueAddedServicesSMSTemplate/valueAddedServicesListSMSTemplate/customSMSTemplateList_page.dart'; import 'starLockApplication/starLockApplication.dart'; +import 'talk/starChart/views/imageTransmission/image_transmission_page.dart'; import 'tools/seletKeyCyclicDate/seletKeyCyclicDate_page.dart'; abstract class Routers { @@ -515,6 +516,8 @@ abstract class Routers { static const String starChartPage = '/starChartPage'; //星图 static const String starChartTalkView = '/starChartTalkView'; //星图对讲页面 static const String h264WebView = '/h264WebView'; //星图对讲页面 + static const String imageTransmissionView = + '/imageTransmissionView'; //星图对讲页面(图传) } abstract class AppRouters { @@ -1185,7 +1188,13 @@ abstract class AppRouters { page: () => const DoubleLockLinkPage()), GetPage( name: Routers.starChartTalkView, page: () => const TalkViewPage()), - GetPage(name: Routers.h264WebView, page: () => TalkViewNativeDecodePage()), // 插件播放页面 + GetPage( + name: Routers.h264WebView, page: () => TalkViewNativeDecodePage()), + // 插件播放页面 + GetPage( + name: Routers.imageTransmissionView, + page: () => ImageTransmissionPage()), + // 插件播放页面 // GetPage(name: Routers.h264WebView, page: () => H264WebView()), // webview播放页面 ]; } diff --git a/lib/talk/starChart/views/imageTransmission/image_transmission_logic.dart b/lib/talk/starChart/views/imageTransmission/image_transmission_logic.dart new file mode 100644 index 00000000..8cc6f854 --- /dev/null +++ b/lib/talk/starChart/views/imageTransmission/image_transmission_logic.dart @@ -0,0 +1,667 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:ui' as ui; +import 'dart:math'; // Import the math package to use sqrt +import 'dart:ui' show decodeImageFromList; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/rendering.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:gallery_saver/gallery_saver.dart'; +import 'package:get/get.dart'; +import 'package:image_gallery_saver/image_gallery_saver.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:star_lock/app_settings/app_settings.dart'; +import 'package:star_lock/login/login/entity/LoginEntity.dart'; +import 'package:star_lock/main/lockDetail/lockDetail/lockDetail_logic.dart'; +import 'package:star_lock/main/lockDetail/lockDetail/lockDetail_state.dart'; +import 'package:star_lock/main/lockDetail/lockDetail/lockNetToken_entity.dart'; +import 'package:star_lock/main/lockDetail/lockSet/lockSet/lockSetInfo_entity.dart'; +import 'package:star_lock/main/lockMian/entity/lockListInfo_entity.dart'; +import 'package:star_lock/network/api_repository.dart'; +import 'package:star_lock/talk/call/g711.dart'; +import 'package:star_lock/talk/starChart/constant/talk_status.dart'; +import 'package:star_lock/talk/starChart/handle/other/talk_data_model.dart'; +import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart'; +import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart'; +import 'package:star_lock/talk/starChart/star_chart_manage.dart'; +import 'package:star_lock/talk/starChart/views/imageTransmission/image_transmission_state.dart'; +import 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart'; +import 'package:star_lock/tools/G711Tool.dart'; +import 'package:star_lock/tools/bugly/bugly_tool.dart'; + +import '../../../../tools/baseGetXController.dart'; + +class ImageTransmissionLogic extends BaseGetXController { + ImageTransmissionState state = ImageTransmissionState(); + + final LockDetailState lockDetailState = Get.put(LockDetailLogic()).state; + + int bufferSize = 8; // 增大缓冲区,满时才渲染 + + int audioBufferSize = 2; // 音频默认缓冲2帧 + bool _isFirstAudioFrame = true; // 是否是第一帧 + + int _startAudioTime = 0; // 开始播放时间戳 + + // 定义音频帧缓冲和发送函数 + final List _bufferedAudioFrames = []; + + // 添加监听状态和订阅引用 + bool _isListening = false; + StreamSubscription? _streamSubscription; + + Timer? videoRenderTimer; // 视频渲染定时器 + + int _renderedFrameCount = 0; + int _lastFpsPrintTime = DateTime.now().millisecondsSinceEpoch; + + /// 初始化音频播放器 + void _initFlutterPcmSound() { + const int sampleRate = 8000; + FlutterPcmSound.setLogLevel(LogLevel.none); + FlutterPcmSound.setup(sampleRate: sampleRate, channelCount: 1); + // 设置 feed 阈值 + if (Platform.isAndroid) { + FlutterPcmSound.setFeedThreshold(1024); // Android 平台的特殊处理 + } else { + FlutterPcmSound.setFeedThreshold(2000); // 非 Android 平台的处理 + } + } + + /// 挂断 + void udpHangUpAction() async { + if (state.talkStatus.value == TalkStatus.answeredSuccessfully) { + // 如果是通话中就挂断 + StartChartManage().startTalkHangupMessageTimer(); + } else { + // 拒绝 + StartChartManage().startTalkRejectMessageTimer(); + } + Get.back(); + } + + // 发起接听命令 + void initiateAnswerCommand() { + StartChartManage().startTalkAcceptTimer(); + } + + // 监听音视频数据流 + void _startListenTalkData() { + // 防止重复监听 + if (_isListening) { + AppLog.log("已经存在数据流监听,避免重复监听"); + return; + } + + AppLog.log("==== 启动新的数据流监听 ===="); + _isListening = true; + _streamSubscription = state.talkDataRepository.talkDataStream + .listen((TalkDataModel talkDataModel) async { + final talkData = talkDataModel.talkData; + final contentType = talkData!.contentType; + final currentTime = DateTime.now().millisecondsSinceEpoch; + + // 判断数据类型,进行分发处理 + switch (contentType) { + case TalkData_ContentTypeE.G711: + // // 第一帧到达时记录开始时间 + if (_isFirstAudioFrame) { + _startAudioTime = currentTime; + _isFirstAudioFrame = false; + } + + // 计算音频延迟 + final expectedTime = _startAudioTime + talkData.durationMs; + final audioDelay = currentTime - expectedTime; + + // 如果延迟太大,清空缓冲区并直接播放 + if (audioDelay > 500) { + state.audioBuffer.clear(); + if (state.isOpenVoice.value) { + _playAudioFrames(); + } + return; + } + if (state.audioBuffer.length >= audioBufferSize) { + state.audioBuffer.removeAt(0); // 丢弃最旧的数据 + } + state.audioBuffer.add(talkData); // 添加新数据 + // 添加音频播放逻辑,与视频类似 + _playAudioFrames(); + break; + case TalkData_ContentTypeE.Image: + // 固定长度缓冲区,最多保留bufferSize帧 + state.videoBuffer.add(talkData); + if (state.videoBuffer.length > bufferSize) { + state.videoBuffer.removeAt(0); // 移除最旧帧 + } + break; + } + }); + } + + // 新增:音频帧播放逻辑 + void _playAudioFrames() { + // 如果缓冲区为空或未达到目标大小,不进行播放 + // 音频缓冲区要求更小,以减少延迟 + if (state.audioBuffer.isEmpty || + state.audioBuffer.length < audioBufferSize) { + return; + } + + // 找出时间戳最小的音频帧 + TalkData? oldestFrame; + int oldestIndex = -1; + for (int i = 0; i < state.audioBuffer.length; i++) { + if (oldestFrame == null || + state.audioBuffer[i].durationMs < oldestFrame.durationMs) { + oldestFrame = state.audioBuffer[i]; + oldestIndex = i; + } + } + + // 确保找到了有效帧 + if (oldestFrame != null && oldestIndex != -1) { + if (state.isOpenVoice.value) { + // 播放音频 + _playAudioData(oldestFrame); + } + state.audioBuffer.removeAt(oldestIndex); + } + } + + /// 监听对讲状态 + 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; + case TalkStatus.answeredSuccessfully: + state.oneMinuteTimeTimer?.cancel(); // 取消旧定时器 + state.oneMinuteTimeTimer ??= + Timer.periodic(const Duration(seconds: 1), (Timer t) { + if (state.listData.value.length > 0) { + state.oneMinuteTime.value++; + // if (state.oneMinuteTime.value >= 60) { + // t.cancel(); // 取消定时器 + // state.oneMinuteTime.value = 0; + // // 倒计时结束挂断 + // // udpHangUpAction(); + // } + } + }); + break; + default: + // 其他状态的处理 + break; + } + }); + } + + /// 播放音频数据 + void _playAudioData(TalkData talkData) async { + if (state.isOpenVoice.value) { + final list = + G711().decodeAndDenoise(talkData.content, true, 8000, 300, 150); + // // 将 PCM 数据转换为 PcmArrayInt16 + final PcmArrayInt16 fromList = PcmArrayInt16.fromList(list); + FlutterPcmSound.feed(fromList); + if (!state.isPlaying.value) { + FlutterPcmSound.play(); + state.isPlaying.value = true; + } + } + } + + /// 停止播放音频 + void _stopPlayG711Data() async { + await FlutterPcmSound.pause(); + await FlutterPcmSound.stop(); + await FlutterPcmSound.clear(); + } + + /// 开门 + // udpOpenDoorAction() async { + // final List? privateKey = + // await Storage.getStringList(saveBluePrivateKey); + // final List getPrivateKeyList = changeStringListToIntList(privateKey!); + // + // final List? signKey = await Storage.getStringList(saveBlueSignKey); + // final List signKeyDataList = changeStringListToIntList(signKey!); + // + // final List? token = await Storage.getStringList(saveBlueToken); + // final List getTokenList = changeStringListToIntList(token!); + // + // await _getLockNetToken(); + // + // final OpenLockCommand openLockCommand = OpenLockCommand( + // lockID: BlueManage().connectDeviceName, + // userID: await Storage.getUid(), + // openMode: lockDetailState.openDoorModel, + // openTime: _getUTCNetTime(), + // onlineToken: lockDetailState.lockNetToken, + // token: getTokenList, + // needAuthor: 1, + // signKey: signKeyDataList, + // privateKey: getPrivateKeyList, + // ); + // final messageDetail = openLockCommand.packageData(); + // // 将 List 转换为十六进制字符串 + // String hexString = messageDetail + // .map((byte) => byte.toRadixString(16).padLeft(2, '0')) + // .join(' '); + // + // AppLog.log('open lock hexString: $hexString'); + // // 发送远程开门消息 + // StartChartManage().sendRemoteUnLockMessage( + // bluetoothDeviceName: BlueManage().connectDeviceName, + // openLockCommand: messageDetail, + // ); + // showToast('正在开锁中...'.tr); + // } + + int _getUTCNetTime() { + if (lockDetailState.isHaveNetwork) { + return DateTime.now().millisecondsSinceEpoch ~/ 1000 + + lockDetailState.differentialTime; + } else { + return 0; + } + } + + /// 获取权限状态 + 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(); + } + } + + Future requestPermissions() async { + // 申请存储权限 + var storageStatus = await Permission.storage.request(); + // 申请录音权限 + var microphoneStatus = await Permission.microphone.request(); + + if (storageStatus.isGranted && microphoneStatus.isGranted) { + print("Permissions granted"); + } else { + print("Permissions denied"); + // 如果权限被拒绝,可以提示用户或跳转到设置页面 + if (await Permission.storage.isPermanentlyDenied) { + openAppSettings(); // 跳转到应用设置页面 + } + } + } + + Future startRecording() async { + // requestPermissions(); + // if (state.isRecordingScreen.value) { + // showToast('录屏已开始,请勿重复点击'); + // } + // bool start = await FlutterScreenRecording.startRecordScreen( + // "Screen Recording", // 视频文件名 + // titleNotification: "Recording in progress", // 通知栏标题 + // messageNotification: "Tap to stop recording", // 通知栏内容 + // ); + // + // if (start) { + // state.isRecordingScreen.value = true; + // } + } + + Future stopRecording() async { + // String path = await FlutterScreenRecording.stopRecordScreen; + // print("Recording saved to: $path"); + // + // // 将视频保存到系统相册 + // bool? success = await GallerySaver.saveVideo(path); + // if (success == true) { + // print("Video saved to gallery"); + // } else { + // print("Failed to save video to gallery"); + // } + // + // showToast('录屏结束,已保存到系统相册'); + // state.isRecordingScreen.value = false; + } + + @override + void onReady() { + super.onReady(); + } + + @override + void onInit() { + super.onInit(); + + // 启动监听音视频数据流 + _startListenTalkData(); + // 启动监听对讲状态 + _startListenTalkStatus(); + // 在没有监听成功之前赋值一遍状态 + // *** 由于页面会在状态变化之后才会初始化,导致识别不到最新的状态,在这里手动赋值 *** + state.talkStatus.value = state.startChartTalkStatus.status; + + // 初始化音频播放器 + _initFlutterPcmSound(); + + // 启动播放定时器 + // _startPlayback(); + + // 初始化录音控制器 + _initAudioRecorder(); + + requestPermissions(); + + // 启动视频渲染定时器(10fps) + videoRenderTimer = Timer.periodic(const Duration(milliseconds: 100), (_) { + final int now = DateTime.now().millisecondsSinceEpoch; + if (state.videoBuffer.isNotEmpty) { + final TalkData oldestFrame = state.videoBuffer.removeAt(0); + if (oldestFrame.content.isNotEmpty) { + state.listData.value = + Uint8List.fromList(oldestFrame.content); // 备份原始数据 + final int decodeStart = DateTime.now().millisecondsSinceEpoch; + decodeImageFromList(Uint8List.fromList(oldestFrame.content)) + .then((ui.Image img) { + final int decodeEnd = DateTime.now().millisecondsSinceEpoch; + state.currentImage.value = img; + _renderedFrameCount++; + // 每秒统计一次fps + if (now - _lastFpsPrintTime >= 1000) { + // print('实际渲染fps: $_renderedFrameCount'); + _renderedFrameCount = 0; + _lastFpsPrintTime = now; + } + }).catchError((e) { + print('图片解码失败: $e'); + }); + } + } + // 如果缓冲区为空,不做任何操作,保持上一次内容 + }); + } + + @override + void onClose() { + _stopPlayG711Data(); // 停止播放音频 + state.listData.value = Uint8List(0); // 清空视频数据 + state.audioBuffer.clear(); // 清空音频缓冲区 + state.videoBuffer.clear(); // 清空视频缓冲区 + + state.oneMinuteTimeTimer?.cancel(); + state.oneMinuteTimeTimer = null; + + stopProcessingAudio(); + // 清理图片缓存 + // _imageCache.clear(); + state.oneMinuteTimeTimer?.cancel(); // 取消旧定时器 + state.oneMinuteTimeTimer = null; // 取消旧定时器 + state.oneMinuteTime.value = 0; + // 取消数据流监听 + _streamSubscription?.cancel(); + _isListening = false; + + // 释放视频渲染定时器 + videoRenderTimer?.cancel(); + videoRenderTimer = null; + + super.onClose(); + } + + @override + void dispose() { + stopProcessingAudio(); + // 重置期望数据 + StartChartManage().reSetDefaultTalkExpect(); + // 释放视频渲染定时器 + videoRenderTimer?.cancel(); + videoRenderTimer = null; + super.dispose(); + } + + /// 处理无效通话状态 + void _handleInvalidTalkStatus() { + state.listData.value = Uint8List(0); + // 停止播放音频 + _stopPlayG711Data(); + stopProcessingAudio(); + } + + /// 更新发送预期数据 + void updateTalkExpect() { + TalkExpectReq talkExpectReq = TalkExpectReq(); + state.isOpenVoice.value = !state.isOpenVoice.value; + if (!state.isOpenVoice.value) { + talkExpectReq = TalkExpectReq( + videoType: [VideoTypeE.IMAGE], + audioType: [], + ); + showToast('已静音'.tr); + } else { + talkExpectReq = TalkExpectReq( + videoType: [VideoTypeE.IMAGE], + audioType: [AudioTypeE.G711], + ); + } + + /// 修改发送预期数据 + StartChartManage().changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer( + talkExpect: talkExpectReq); + } + + /// 截图并保存到相册 + 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'); + showToast('截图已保存到相册'.tr); + } catch (e) { + AppLog.log('截图失败: $e'); + } + } + + // 远程开锁 + Future remoteOpenLock() async { + final lockPeerId = StartChartManage().lockPeerId; + final lockListPeerId = StartChartManage().lockListPeerId; + int lockId = lockDetailState.keyInfos.value.lockId ?? 0; + + // 如果锁列表获取到peerId,代表有多个锁,使用锁列表的peerId + // 从列表中遍历出对应的peerId + lockListPeerId.forEach((element) { + if (element.network?.peerId == lockPeerId) { + lockId = element.lockId ?? 0; + } + }); + + final LockSetInfoEntity lockSetInfoEntity = + await ApiRepository.to.getLockSettingInfoData( + lockId: lockId.toString(), + ); + if (lockSetInfoEntity.errorCode!.codeIsSuccessful) { + if (lockSetInfoEntity.data?.lockFeature?.remoteUnlock == 1 && + lockSetInfoEntity.data?.lockSettingInfo?.remoteUnlock == 1) { + final LoginEntity entity = await ApiRepository.to + .remoteOpenLock(lockId: lockId.toString(), timeOut: 60); + if (entity.errorCode!.codeIsSuccessful) { + showToast('已开锁'.tr); + StartChartManage().lockListPeerId = []; + } + } else { + showToast('该锁的远程开锁功能未启用'.tr); + } + } + } + + /// 初始化音频录制器 + void _initAudioRecorder() { + state.voiceProcessor = VoiceProcessor.instance; + } + + //开始录音 + Future startProcessingAudio() async { + try { + if (await state.voiceProcessor?.hasRecordAudioPermission() ?? false) { + await state.voiceProcessor?.start(state.frameLength, state.sampleRate); + final bool? isRecording = await state.voiceProcessor?.isRecording(); + state.isRecordingAudio.value = isRecording!; + state.startRecordingAudioTime.value = DateTime.now(); + + // 增加录音帧监听器和错误监听器 + state.voiceProcessor + ?.addFrameListeners([_onFrame]); + state.voiceProcessor?.addErrorListener(_onError); + } else { + // state.errorMessage.value = 'Recording permission not granted'; + } + } on PlatformException catch (ex) { + // state.errorMessage.value = 'Failed to start recorder: $ex'; + } + state.isOpenVoice.value = false; + } + + /// 停止录音 + Future stopProcessingAudio() async { + try { + await state.voiceProcessor?.stop(); + state.voiceProcessor?.removeFrameListener(_onFrame); + state.udpSendDataFrameNumber = 0; + // 记录结束时间 + state.endRecordingAudioTime.value = DateTime.now(); + + // 计算录音的持续时间 + final Duration duration = state.endRecordingAudioTime.value + .difference(state.startRecordingAudioTime.value); + + state.recordingAudioTime.value = duration.inSeconds; + } on PlatformException catch (ex) { + // state.errorMessage.value = 'Failed to stop recorder: $ex'; + } finally { + final bool? isRecording = await state.voiceProcessor?.isRecording(); + state.isRecordingAudio.value = isRecording!; + state.isOpenVoice.value = true; + } + } + +// 音频帧处理 + Future _onFrame(List frame) async { + // 添加最大缓冲限制 + if (_bufferedAudioFrames.length > state.frameLength * 3) { + _bufferedAudioFrames.clear(); // 清空过多积累的数据 + return; + } + + // 首先应用固定增益提升基础音量 + List amplifiedFrame = _applyGain(frame, 1.6); + // 编码为G711数据 + List encodedData = G711Tool.encode(amplifiedFrame, 0); // 0表示A-law + _bufferedAudioFrames.addAll(encodedData); + // 使用相对时间戳 + final int ms = DateTime.now().millisecondsSinceEpoch % 1000000; // 使用循环时间戳 + int getFrameLength = state.frameLength; + if (Platform.isIOS) { + getFrameLength = state.frameLength * 2; + } + + // 添加发送间隔控制 + if (_bufferedAudioFrames.length >= state.frameLength) { + try { + await StartChartManage().sendTalkDataMessage( + talkData: TalkData( + content: _bufferedAudioFrames, + contentType: TalkData_ContentTypeE.G711, + durationMs: ms, + ), + ); + } finally { + _bufferedAudioFrames.clear(); // 确保清理缓冲区 + } + } else { + _bufferedAudioFrames.addAll(encodedData); + } + } + +// 错误监听 + void _onError(VoiceProcessorException error) { + AppLog.log(error.message!); + } + +// 添加音频增益处理方法 + List _applyGain(List pcmData, double gainFactor) { + List result = List.filled(pcmData.length, 0); + + for (int i = 0; i < pcmData.length; i++) { + // PCM数据通常是有符号的16位整数 + int sample = pcmData[i]; + + // 应用增益 + double amplified = sample * gainFactor; + + // 限制在有效范围内,防止溢出 + if (amplified > 32767) { + amplified = 32767; + } else if (amplified < -32768) { + amplified = -32768; + } + + result[i] = amplified.toInt(); + } + + return result; + } +} diff --git a/lib/talk/starChart/views/imageTransmission/image_transmission_page.dart b/lib/talk/starChart/views/imageTransmission/image_transmission_page.dart new file mode 100644 index 00000000..c7ce2678 --- /dev/null +++ b/lib/talk/starChart/views/imageTransmission/image_transmission_page.dart @@ -0,0 +1,238 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:star_lock/app_settings/app_colors.dart'; +import 'package:star_lock/talk/call/callTalk.dart'; +import 'package:star_lock/talk/starChart/constant/talk_status.dart'; +import 'package:star_lock/talk/starChart/star_chart_manage.dart'; +import 'package:star_lock/talk/starChart/views/imageTransmission/image_transmission_logic.dart'; +import 'package:star_lock/talk/starChart/views/imageTransmission/image_transmission_state.dart'; +import 'package:star_lock/tools/titleAppBar.dart'; +import 'package:slide_to_act/slide_to_act.dart'; + +// 可选:引入第三方滑动解锁库 +// import 'package:flutter_slider_button/flutter_slider_button.dart'; + +class ImageTransmissionPage extends StatefulWidget { + const ImageTransmissionPage(); + + @override + State createState() => _ImageTransmissionPageState(); +} + +class _ImageTransmissionPageState extends State + with TickerProviderStateMixin { + final ImageTransmissionLogic logic = Get.put(ImageTransmissionLogic()); + final ImageTransmissionState state = Get.find().state; + final startChartManage = StartChartManage(); + + @override + void initState() { + super.initState(); + state.animationController = AnimationController( + vsync: this, // 确保使用的TickerProvider是当前Widget + duration: const Duration(seconds: 1), + ); + state.animationController.repeat(); + state.animationController.addStatusListener((AnimationStatus status) { + if (status == AnimationStatus.completed) { + state.animationController.reset(); + state.animationController.forward(); + } else if (status == AnimationStatus.dismissed) { + state.animationController.reset(); + state.animationController.forward(); + } + }); + } + + @override + void dispose() { + state.animationController.dispose(); + CallTalk().finishAVData(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: AppColors.mainBackgroundColor, + resizeToAvoidBottomInset: false, + appBar: TitleAppBar( + barTitle: '图传全自动'.tr, + haveBack: true, + backgroundColor: AppColors.mainColor, + backAction: (){ + logic.udpHangUpAction(); + }, + ), + body: Obx(() => Column( + children: [ + SizedBox(height: 24.h), + SizedBox( + height: 0.6.sh, + child: state.listData.value.isEmpty + ? _buildWaitingView() + : _buildVideoView(), + ), + SizedBox(height: 30.h), + _buildBottomToolBar(), + SizedBox(height: 30.h), + ], + )), + ); + } + + Widget _buildWaitingView() { + double barWidth = MediaQuery.of(context).size.width - 60.w; + return Center( + child: ClipRRect( + borderRadius: BorderRadius.circular(30.h), + child: Stack( + alignment: Alignment.center, + children: [ + Container( + width: barWidth, + height: double.infinity, + child: Image.asset( + 'images/main/monitorBg.png', + fit: BoxFit.cover, + ), + ), + RotationTransition( + turns: state.animationController, + child: Image.asset( + 'images/main/realTime_connecting.png', + width: 300.w, + height: 300.w, + fit: BoxFit.contain, + ), + ), + ], + ), + ), + ); + } + + Widget _buildVideoView() { + double barWidth = MediaQuery.of(context).size.width - 60.w; + return PopScope( + canPop: false, + child: RepaintBoundary( + key: state.globalKey, + child: Center( + child: ClipRRect( + borderRadius: BorderRadius.circular(30.h), + child: Container( + width: barWidth, + height: double.infinity, + child: RotatedBox( + quarterTurns: startChartManage.rotateAngle ~/ 90, + child: RawImage( + image: state.currentImage.value, + fit: BoxFit.cover, + filterQuality: FilterQuality.high, + ), + ), + ), + ), + ), + ), + ); + } + + Widget _buildBottomToolBar() { + return Container( + margin: EdgeInsets.symmetric(horizontal: 30.w), + padding: EdgeInsets.symmetric(vertical: 28.h, horizontal: 20.w), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(30.h), + boxShadow: [ + BoxShadow( + color: Colors.black12, + blurRadius: 12, + offset: Offset(0, 4), + ), + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _circleButton( + icon: Icons.call, + color: Colors.green, + onTap: () { + if (state.talkStatus.value == + TalkStatus.passiveCallWaitingAnswer) { + // 接听 + logic.initiateAnswerCommand(); + } + }, + ), + _circleButton( + icon: Icons.call_end, + color: Colors.red, + onTap: () { + logic.udpHangUpAction(); + }, + ), + _circleButton( + icon: Icons.camera_alt, + color: Colors.blue, + onTap: () async { + if (state.talkStatus.value == + TalkStatus.answeredSuccessfully) { + await logic.captureAndSavePng(); + } + }, + ), + ], + ), + SizedBox(height: 36.h), + SlideAction( + height: 64.h, + borderRadius: 24.h, + elevation: 0, + innerColor: Colors.amber, + outerColor: Colors.amber.withOpacity(0.15), + sliderButtonIcon: Icon(Icons.lock, color: Colors.white, size: 40.w), + text: '滑动解锁', + textStyle: TextStyle(fontSize: 26.sp, color: Colors.black54, fontWeight: FontWeight.bold), + onSubmit: () { + // TODO: 实现滑动解锁逻辑 + logic.remoteOpenLock(); + }, + ), + ], + ), + ); + } + + Widget _circleButton( + {required IconData icon, + required Color color, + required VoidCallback onTap}) { + return GestureDetector( + onTap: onTap, + child: Container( + width: 90.w, + height: 90.w, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: color.withOpacity(0.3), + blurRadius: 10, + offset: Offset(0, 4), + ), + ], + ), + child: Icon(icon, color: Colors.white, size: 48.w), + ), + ); + } +} diff --git a/lib/talk/starChart/views/imageTransmission/image_transmission_state.dart b/lib/talk/starChart/views/imageTransmission/image_transmission_state.dart new file mode 100644 index 00000000..a7cd0efc --- /dev/null +++ b/lib/talk/starChart/views/imageTransmission/image_transmission_state.dart @@ -0,0 +1,94 @@ +import 'dart:async'; +import 'dart:typed_data'; +import 'dart:ui' as ui; + +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:get/get_rx/src/rx_types/rx_types.dart'; +import 'package:get/state_manager.dart'; +import 'package:network_info_plus/network_info_plus.dart'; +import 'package:star_lock/talk/starChart/constant/talk_status.dart'; +import 'package:star_lock/talk/starChart/handle/other/talk_data_repository.dart'; +import 'package:star_lock/talk/starChart/proto/talk_data.pb.dart'; +import 'package:star_lock/talk/starChart/status/star_chart_talk_status.dart'; + +import '../../../../tools/storage.dart'; + +enum NetworkStatus { + normal, // 0 + lagging, // 1 + delayed, // 2 + packetLoss // 3 +} + +class ImageTransmissionState{ + 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; //得到的音频流字节数据 + GlobalKey globalKey = GlobalKey(); + + Timer? oneMinuteTimeTimer; // 定时器超过60秒关闭当前界面 + RxInt oneMinuteTime = 0.obs; // 定时器秒数 + + // 定时器如果发送了接听的命令 而没收到回复就每秒重复发送10次 + late Timer answerTimer; + late Timer hangUpTimer; + late Timer openDoorTimer; + Timer? fpsTimer; + late AnimationController animationController; + + 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 activeAudioBuffer = [].obs; + List activeVideoBuffer = [].obs; + + List videoBuffer = [].obs; + List videoBuffer2 = [].obs; + RxBool isPlaying = false.obs; // 是否开始播放 + Rx talkStatus = TalkStatus.none.obs; //星图对讲状态 + // 获取 startChartTalkStatus 的唯一实例 + final StartChartTalkStatus startChartTalkStatus = + StartChartTalkStatus.instance; + + // 通话数据流的单例流数据处理类 + final TalkDataRepository talkDataRepository = TalkDataRepository.instance; + RxInt lastFrameTimestamp = 0.obs; // 上一帧的时间戳,用来判断网络环境 + Rx networkStatus = + NetworkStatus.normal.obs; // 网络状态:0-正常 1-网络卡顿 2-网络延迟 3-网络丢包 + RxInt alertCount = 0.obs; // 网络状态提示计数器 + RxInt maxAlertNumber = 3.obs; // 网络状态提示最大提示次数 + RxBool isOpenVoice = true.obs; // 是否打开声音 + RxBool isRecordingScreen = false.obs; // 是否录屏中 + RxBool isRecordingAudio = false.obs; // 是否录音中 + Rx startRecordingAudioTime = DateTime.now().obs; // 开始录音时间 + Rx endRecordingAudioTime = DateTime.now().obs; // 结束录音时间 + RxInt recordingAudioTime = 0.obs; // 录音时间持续时间 + RxInt fps = 0.obs; // 添加 FPS 计数 + late VoiceProcessor? voiceProcessor; // 音频处理器、录音 + final int frameLength = 320; //录音视频帧长度为640 + final int sampleRate = 8000; //录音频采样率为8000 + List recordingAudioAllFrames = []; // 录制音频的所有帧 + List lockRecordingAudioAllFrames = []; // 录制音频的所有帧 + RxInt rotateAngle = 0.obs; // 旋转角度(以弧度为单位) + RxBool isLongPressing = false.obs; // 旋转角度(以弧度为单位) + RxBool hasAudioData = false.obs; // 是否有音频数据 + RxInt lastAudioTimestamp = 0.obs; // 最后接收到的音频数据的时间戳 + Rx currentImage = Rx(null); +} \ No newline at end of file From 6dc16276221b63ef3413dae330a1fde194aaacb6 Mon Sep 17 00:00:00 2001 From: liyi Date: Tue, 13 May 2025 09:47:35 +0800 Subject: [PATCH 091/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E8=93=9D?= =?UTF-8?q?=E7=89=99=E5=91=BD=E4=BB=A4=E6=97=B6=E7=9A=84=E6=90=9C=E7=B4=A2?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/blue/blue_manage.dart | 48 +++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/lib/blue/blue_manage.dart b/lib/blue/blue_manage.dart index 0259a5c0..a955171b 100755 --- a/lib/blue/blue_manage.dart +++ b/lib/blue/blue_manage.dart @@ -187,10 +187,12 @@ class BlueManage { continue; } - final isMatch = _isMatch(scanResult - .advertisementData.serviceUuids - .map((e) => e.uuid) - .toList()); + final isMatch = _isMatch( + scanResult.advertisementData.serviceUuids + .map((e) => e.uuid) + .toList(), + isSingle: true, + ); if (isMatch && (scanResult.rssi >= -100)) { // 查询id相同的元素 @@ -273,6 +275,7 @@ class BlueManage { .map((e) => e.uuid) .toList(), deviceType: deviceType, + isSingle: false, ); // 判断名字为空的直接剔除 if (isMatch && (scanResult.rssi >= -100)) { @@ -316,8 +319,10 @@ class BlueManage { } /// 判断是否包含指定的uuid - bool _isMatch(List serviceUuids, {DeviceType deviceType = DeviceType.blue}) { - final List prefixes = getDeviceType(deviceType).map((e) => e.toLowerCase()).toList(); + bool _isMatch(List serviceUuids, + {DeviceType deviceType = DeviceType.blue, required bool isSingle}) { + final List prefixes = + getDeviceType(deviceType).map((e) => e.toLowerCase()).toList(); for (String uuid in serviceUuids) { final String cleanUuid = uuid.toLowerCase(); if (cleanUuid.length == 8) { @@ -325,11 +330,15 @@ class BlueManage { String pairStatus = cleanUuid.substring(4, 6); // 第4、5位(索引3和4) for (final prefix in prefixes) { if (cleanUuid.startsWith(prefix)) { - // 00=未配对,01=已配对 - if (pairStatus == '00') { - return true; // 未配对才返回true + if (isSingle) { + return true; // isSingle为true,前缀匹配即返回true + } else { + // 00=未配对,01=已配对 + if (pairStatus == '00') { + return true; // 未配对才返回true + } + // 已配对(01)不返回true,继续判断下一个uuid } - // 已配对(01)不返回true,继续判断下一个uuid } } } else { @@ -338,14 +347,19 @@ class BlueManage { final String thirdAndFourth = cleanUuid.substring(2, 4); // 索引2和3 for (final prefix in prefixes) { if (thirdAndFourth == prefix) { - // 判断配对状态(带横杠UUID的第31、32位,从1开始计数) - if (cleanUuid.length >= 32) { - String pairStatus = cleanUuid.substring(30, 32); // 第31、32位(从1开始计数) - // 00=未配对,01=已配对 - if (pairStatus == '00') { - return true; // 未配对才返回true + if (isSingle) { + return true; // isSingle为true,前缀匹配即返回true + } else { + // 判断配对状态(带横杠UUID的第31、32位,从1开始计数) + if (cleanUuid.length >= 32) { + String pairStatus = + cleanUuid.substring(30, 32); // 第31、32位(从1开始计数) + // 00=未配对,01=已配对 + if (pairStatus == '00') { + return true; // 未配对才返回true + } + // 已配对(01)不返回true,继续判断下一个uuid } - // 已配对(01)不返回true,继续判断下一个uuid } } } From 57a171b993cdf1e5bff347521d0dacf22f4bc128 Mon Sep 17 00:00:00 2001 From: liyi Date: Tue, 13 May 2025 09:47:55 +0800 Subject: [PATCH 092/151] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0=E5=AF=86?= =?UTF-8?q?=E7=A0=81=E6=8C=89=E7=85=A7=E9=94=81=E6=94=AF=E6=8C=81=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lockDetail/lockDetail/lockDetail_page.dart | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/main/lockDetail/lockDetail/lockDetail_page.dart b/lib/main/lockDetail/lockDetail/lockDetail_page.dart index ed3c9b68..f7605c48 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_page.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_page.dart @@ -1103,13 +1103,15 @@ class _LockDetailPageState extends State })); // 密码 - showWidgetArr.add(bottomItem('images/main/icon_main_password.png', '密码'.tr, - state.bottomBtnisEable.value, () { - Get.toNamed(Routers.passwordKeyListPage, - arguments: { - 'keyInfo': state.keyInfos.value - }); - })); + if (state.keyInfos.value.lockFeature!.password == 1) { + showWidgetArr.add(bottomItem('images/main/icon_main_password.png', + '密码'.tr, state.bottomBtnisEable.value, () { + Get.toNamed(Routers.passwordKeyListPage, + arguments: { + 'keyInfo': state.keyInfos.value + }); + })); + } // ic卡 if (state.keyInfos.value.lockFeature!.icCard == 1) { From f887bd37c436452e283470062e7dd77f1f588e88 Mon Sep 17 00:00:00 2001 From: liyi Date: Tue, 13 May 2025 09:48:08 +0800 Subject: [PATCH 093/151] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E9=A1=B9=E5=B1=9E=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/main/lockMian/entity/lockListInfo_entity.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/main/lockMian/entity/lockListInfo_entity.dart b/lib/main/lockMian/entity/lockListInfo_entity.dart index b98bb328..28c48457 100755 --- a/lib/main/lockMian/entity/lockListInfo_entity.dart +++ b/lib/main/lockMian/entity/lockListInfo_entity.dart @@ -361,6 +361,7 @@ class Bluetooth { class LockFeature { LockFeature({ this.password, + this.passwordIssue, this.icCard, this.fingerprint, this.fingerVein, @@ -381,6 +382,7 @@ class LockFeature { LockFeature.fromJson(Map json) { password = json['password']; + passwordIssue = json['passwordIssue']; icCard = json['icCard']; fingerprint = json['fingerprint']; fingerVein = json['fingerVein']; @@ -400,6 +402,7 @@ class LockFeature { } int? password; + int? passwordIssue; int? icCard; int? fingerprint; int? fingerVein; @@ -420,6 +423,7 @@ class LockFeature { Map toJson() { final Map data = {}; data['password'] = password; + data['passwordIssue'] = passwordIssue; data['icCard'] = icCard; data['fingerprint'] = fingerprint; data['fingerVein'] = fingerVein; From 06fc544f1a120b6810a6dabff00f321ad2c801ab Mon Sep 17 00:00:00 2001 From: liyi Date: Tue, 13 May 2025 10:05:05 +0800 Subject: [PATCH 094/151] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E5=88=86=E8=BE=A8=E7=8E=87=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handle/impl/udp_talk_expect_handler.dart | 4 +- lib/talk/starChart/star_chart_manage.dart | 2 + .../views/talkView/talk_view_page.dart | 165 ++++++++++++------ 3 files changed, 120 insertions(+), 51 deletions(-) diff --git a/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart index e9f3e598..792e7f2c 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_expect_handler.dart @@ -43,7 +43,9 @@ class UdpTalkExpectHandler extends ScpMessageBaseHandle startChartManage.stopCallRequestMessageTimer(); // talkViewState.rotateAngle.value = talkExpectResp.rotate ?? 0; startChartManage.rotateAngle = talkExpectResp.rotate; - AppLog.log('视频画面需要旋转:${talkExpectResp.rotate}'); + startChartManage.videoWidth = talkExpectResp.width; + startChartManage.videoHeight = talkExpectResp.height; + AppLog.log('视频画面需要旋转:${talkExpectResp.rotate},画面宽高:${talkExpectResp.width}-${talkExpectResp.height}'); // 收到预期数据的应答后,代表建立了连接,启动通话保持的监听 // 启动通话保持监听定时器(用来判断如果x秒内没有收到通话保持则执行的操作); talkePingOverTimeTimerManager.start(); diff --git a/lib/talk/starChart/star_chart_manage.dart b/lib/talk/starChart/star_chart_manage.dart index e5eddb59..d64cb808 100644 --- a/lib/talk/starChart/star_chart_manage.dart +++ b/lib/talk/starChart/star_chart_manage.dart @@ -114,6 +114,8 @@ class StartChartManage { final int _maxPayloadSize = 8 * 1024; // 分包大小 int rotateAngle = 0; // 视频旋转角度 + int videoWidth = 0; // 视频宽度 + int videoHeight = 0; // 视频高度 // 默认通话的期望数据格式 TalkExpectReq _defaultTalkExpect = TalkConstant.H264Expect; diff --git a/lib/talk/starChart/views/talkView/talk_view_page.dart b/lib/talk/starChart/views/talkView/talk_view_page.dart index 264e0252..7abb28b1 100644 --- a/lib/talk/starChart/views/talkView/talk_view_page.dart +++ b/lib/talk/starChart/views/talkView/talk_view_page.dart @@ -98,56 +98,55 @@ class _TalkViewPageState extends State child: Stack( alignment: Alignment.center, children: [ - Obx( - () { - final double screenWidth = MediaQuery.of(context).size.width; - final double screenHeight = MediaQuery.of(context).size.height; - - final double logicalWidth = MediaQuery.of(context).size.width; - final double logicalHeight = MediaQuery.of(context).size.height; - final double devicePixelRatio = - MediaQuery.of(context).devicePixelRatio; - - // 计算物理像素值 - final double physicalWidth = logicalWidth * devicePixelRatio; - final double physicalHeight = logicalHeight * devicePixelRatio; - - // 旋转后的图片尺寸 - const int rotatedImageWidth = 480; // 原始高度 - const int rotatedImageHeight = 864; // 原始宽度 - - // 计算缩放比例 - final double scaleWidth = physicalWidth / rotatedImageWidth; - final double scaleHeight = physicalHeight / rotatedImageHeight; - max(scaleWidth, scaleHeight); // 选择较大的缩放比例 - - return state.listData.value.isEmpty - ? Image.asset( - 'images/main/monitorBg.png', - width: screenWidth, - height: screenHeight, - fit: BoxFit.cover, - ) - : PopScope( - canPop: false, - child: RepaintBoundary( - key: state.globalKey, - child: SizedBox.expand( - child: RotatedBox( - quarterTurns: startChartManage.rotateAngle ~/ 90, - child: RawImage( - image: state.currentImage.value, - width: ScreenUtil().scaleWidth, - height: ScreenUtil().scaleHeight, - fit: BoxFit.cover, - filterQuality: FilterQuality.high, - ), - ), - ), - ), - ); - }, - ), + // 全屏背景图片或渐变色背景 + Obx(() { + if (state.listData.value.isEmpty) { + return SizedBox.expand( + child: Image.asset( + 'images/main/monitorBg.png', + fit: BoxFit.cover, + ), + ); + } + final int videoW = startChartManage.videoWidth; + final int videoH = startChartManage.videoHeight; + if (videoW == 320 && videoH == 240) { + return SizedBox.expand( + child: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFF232526), + Color(0xFF414345), + ], + ), + ), + ), + ); + } + return const SizedBox.shrink(); + }), + // 视频窗口,分辨率判断 + Obx(() { + if (state.listData.value.isEmpty) { + return const SizedBox.shrink(); + } + final int videoW = startChartManage.videoWidth; + final int videoH = startChartManage.videoHeight; + if (videoW == 320 && videoH == 240) { + return Positioned( + top: 150.h, + left: 0, + right: 0, + child: _buildVideoWidget(), + ); + } else { + // 直接全屏显示 + return _buildVideoWidget(); + } + }), Obx(() => state.listData.value.isEmpty ? Positioned( bottom: 310.h, @@ -183,6 +182,8 @@ class _TalkViewPageState extends State ), ) : Container()), + + /// 工具栏 Positioned( bottom: 10.w, child: Container( @@ -614,4 +615,68 @@ class _TalkViewPageState extends State // UdpTalkDataHandler().resetDataRates(); super.dispose(); } + + Widget _buildVideoWidget() { + // 工具栏宽度 + double barWidth = 1.sw - 30.w * 2; + int videoW = startChartManage.videoWidth; + int videoH = startChartManage.videoHeight; + int quarterTurns = startChartManage.rotateAngle ~/ 90; + bool isRotated = quarterTurns % 2 == 1; + // 旋转后宽高互换 + double videoAspect = isRotated ? videoW / videoH : videoH / videoW; + double containerHeight = + barWidth * (isRotated ? videoW / videoH : videoH / videoW); + + if (videoW == 320 && videoH == 240) { + return Center( + child: ClipRRect( + borderRadius: BorderRadius.circular(20.h), + child: Container( + width: barWidth, + height: containerHeight, + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFF232526), + Color(0xFF414345), + ], + ), + ), + child: RotatedBox( + quarterTurns: quarterTurns, + child: RawImage( + image: state.currentImage.value, + fit: BoxFit.contain, + filterQuality: FilterQuality.high, + width: barWidth, + height: containerHeight, + ), + ), + ), + ), + ); + } else { + return PopScope( + canPop: false, + child: RepaintBoundary( + key: state.globalKey, + child: SizedBox.expand( + child: RotatedBox( + quarterTurns: startChartManage.rotateAngle ~/ 90, + child: RawImage( + image: state.currentImage.value, + width: ScreenUtil().scaleWidth, + height: ScreenUtil().scaleHeight, + fit: BoxFit.cover, + filterQuality: FilterQuality.high, + ), + ), + ), + ), + ); + } + } } From a7a70f41b1d43329f0953896f1005ef7106e543d Mon Sep 17 00:00:00 2001 From: liyi Date: Tue, 13 May 2025 10:19:43 +0800 Subject: [PATCH 095/151] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E5=88=86=E8=BE=A8=E7=8E=87=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handle/impl/udp_talk_accept_handler.dart | 31 +++++++-- .../handle/impl/udp_talk_request_handler.dart | 66 +++++++++++++++++-- 2 files changed, 87 insertions(+), 10 deletions(-) diff --git a/lib/talk/starChart/handle/impl/udp_talk_accept_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_accept_handler.dart index baaae2e8..35389a70 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_accept_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_accept_handler.dart @@ -15,6 +15,7 @@ import 'package:star_lock/talk/starChart/proto/generic.pb.dart'; import 'package:star_lock/talk/starChart/proto/talk_accept.pb.dart'; import 'package:star_lock/talk/starChart/proto/talk_expect.pb.dart'; import 'package:star_lock/tools/commonDataManage.dart'; +import 'package:star_lock/tools/storage.dart'; import '../../star_chart_manage.dart'; @@ -34,7 +35,7 @@ class UdpTalkAcceptHandler extends ScpMessageBaseHandle // 停止同意接听的重发 startChartManage.stopTalkAcceptTimer(); // 接听之后增加期望音频的接收 - _handleSendExpect(); + _handleSendExpect(lockPeerID: scpMessage.FromPeerId!); // 停止播放铃声 stopRingtone(); // 设置状态为接听成功 @@ -79,11 +80,33 @@ class UdpTalkAcceptHandler extends ScpMessageBaseHandle } /// 收到同意接听回复之后增加音频的期望数据 - void _handleSendExpect() { + void _handleSendExpect({ + required String lockPeerID, + }) async { final LockListInfoItemEntity currentKeyInfo = CommonDataManage().currentKeyInfo; - final isH264 = currentKeyInfo.lockFeature?.isH264 == 1; - final isMJpeg = currentKeyInfo.lockFeature?.isMJpeg == 1; + + var isH264 = currentKeyInfo.lockFeature?.isH264 == 1; + var isMJpeg = currentKeyInfo.lockFeature?.isMJpeg == 1; + + final LockListInfoGroupEntity? lockListInfoGroupEntity = + await Storage.getLockMainListData(); + if (lockListInfoGroupEntity != null) { + lockListInfoGroupEntity!.groupList?.forEach((element) { + final lockList = element.lockList; + if (lockList != null && lockList.length != 0) { + for (var lockInfo in lockList) { + final peerId = lockInfo.network?.peerId; + if (peerId != null && peerId != '') { + if (peerId == lockPeerID) { + isH264 = lockInfo.lockFeature?.isH264 == 1; + isMJpeg = lockInfo.lockFeature?.isMJpeg == 1; + } + } + } + } + }); + } // 优先使用H264,其次是MJPEG if (isH264) { diff --git a/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart index 55a2ac27..7ce19cf6 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart @@ -37,7 +37,10 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle startChartManage.ToPeerId = scpMessage.FromPeerId!; startChartManage.lockPeerId = scpMessage.FromPeerId!; // 处理收到接听请求后的事件 - _talkRequestEvent(talkObjectName: talkReq.callerName); + _talkRequestEvent( + talkObjectName: talkReq.callerName, + lockPeerID: scpMessage.FromPeerId!, + ); // 回复成功 replySuccessMessage(scpMessage); @@ -78,9 +81,12 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle void handleRealTimeData(ScpMessage scpMessage) {} // 来电事件的处理 - void _talkRequestEvent({required String talkObjectName}) { + void _talkRequestEvent({ + required String talkObjectName, + required String lockPeerID, + }) async { // 发送预期数据、通知锁板需要获取视频数据 - _handleRequestSendExpect(); + _handleRequestSendExpect(lockPeerID: lockPeerID); // 播放铃声 //test:使用自定义铃声 playRingtone(); @@ -88,6 +94,33 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle // _showTalkRequestNotification(talkObjectName: talkObjectName); // 设置为等待接听状态 talkStatus.setPassiveCallWaitingAnswer(); + + // 获取锁支持项 + final LockListInfoItemEntity currentKeyInfo = + CommonDataManage().currentKeyInfo; + var isWifiLockType = currentKeyInfo.lockFeature?.wifiLockType == 1; + + final LockListInfoGroupEntity? lockListInfoGroupEntity = + await Storage.getLockMainListData(); + if (lockListInfoGroupEntity != null) { + lockListInfoGroupEntity!.groupList?.forEach((element) { + final lockList = element.lockList; + if (lockList != null && lockList.length != 0) { + for (var lockInfo in lockList) { + final peerId = lockInfo.network?.peerId; + if (peerId != null && peerId != '') { + if (peerId == lockPeerID) { + isWifiLockType = lockInfo.lockFeature?.wifiLockType == 1; + } + } + } + } + }); + } + if (isWifiLockType) { + Get.toNamed(Routers.imageTransmissionView); + return; + } if (startChartManage .getDefaultTalkExpect() .videoType @@ -170,12 +203,33 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle } /// app收到的对讲请求后,发送的预期数据 - void _handleRequestSendExpect() { + void _handleRequestSendExpect({ + required String lockPeerID, + }) async { final LockListInfoItemEntity currentKeyInfo = CommonDataManage().currentKeyInfo; - final isH264 = currentKeyInfo.lockFeature?.isH264 == 1; - final isMJpeg = currentKeyInfo.lockFeature?.isMJpeg == 1; + var isH264 = currentKeyInfo.lockFeature?.isH264 == 1; + var isMJpeg = currentKeyInfo.lockFeature?.isMJpeg == 1; + + final LockListInfoGroupEntity? lockListInfoGroupEntity = + await Storage.getLockMainListData(); + if (lockListInfoGroupEntity != null) { + lockListInfoGroupEntity!.groupList?.forEach((element) { + final lockList = element.lockList; + if (lockList != null && lockList.length != 0) { + for (var lockInfo in lockList) { + final peerId = lockInfo.network?.peerId; + if (peerId != null && peerId != '') { + if (peerId == lockPeerID) { + isH264 = lockInfo.lockFeature?.isH264 == 1; + isMJpeg = lockInfo.lockFeature?.isMJpeg == 1; + } + } + } + } + }); + } // 优先使用H264,其次是MJPEG if (isH264) { // 锁支持H264,发送H264视频和G711音频期望 From 7d27de087d8f47ccc778f35cb647ab7bcb5987c3 Mon Sep 17 00:00:00 2001 From: liyi Date: Tue, 13 May 2025 14:30:10 +0800 Subject: [PATCH 096/151] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0=E6=BB=91?= =?UTF-8?q?=E5=8A=A8=E7=AA=97=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pubspec.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pubspec.yaml b/pubspec.yaml index 09ba91d5..68d2d366 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -280,6 +280,8 @@ dependencies: video_thumbnail: ^0.5.3 # 角标管理 flutter_app_badger: ^1.3.0 + # 滑块支持 + slide_to_act: ^2.0.2 From 160c4d33acaeeaf9ef316c36029714b3d2f3454e Mon Sep 17 00:00:00 2001 From: liyi Date: Wed, 14 May 2025 09:08:41 +0800 Subject: [PATCH 097/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E7=BC=93?= =?UTF-8?q?=E5=86=B2=E5=8C=BA=E5=A4=A7=E5=B0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lockDetail/lockDetail_logic.dart | 2 +- lib/talk/starChart/star_chart_manage.dart | 6 ++- .../native/talk_view_native_decode_logic.dart | 37 +++++++++++-------- .../native/talk_view_native_decode_state.dart | 2 +- 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/lib/main/lockDetail/lockDetail/lockDetail_logic.dart b/lib/main/lockDetail/lockDetail/lockDetail_logic.dart index 7c086ba5..cf9eda27 100755 --- a/lib/main/lockDetail/lockDetail/lockDetail_logic.dart +++ b/lib/main/lockDetail/lockDetail/lockDetail_logic.dart @@ -773,7 +773,7 @@ class LockDetailLogic extends BaseGetXController { return; } // 重置丢包率监控 - PacketLossStatistics().reset(); + // PacketLossStatistics().reset(); // 发送监控id StartChartManage() .startCallRequestMessageTimer(ToPeerId: network!.peerId ?? ''); diff --git a/lib/talk/starChart/star_chart_manage.dart b/lib/talk/starChart/star_chart_manage.dart index d64cb808..2d8b4529 100644 --- a/lib/talk/starChart/star_chart_manage.dart +++ b/lib/talk/starChart/star_chart_manage.dart @@ -606,7 +606,7 @@ class StartChartManage { void startTalkRejectMessageTimer() async { try { int count = 0; - final int maxCount = 10; // 最大执行次数为10秒 + final int maxCount = 3; // 最大执行次数为10秒 talkRejectTimer ??= Timer.periodic( Duration(seconds: _defaultIntervalTime), @@ -632,6 +632,8 @@ class StartChartManage { stopCallRequestMessageTimer(); stopSendingRbcuInfoMessages(); stopSendingRbcuProBeMessages(); + stopTalkAcceptTimer(); + stopCallRequestMessageTimer(); // 取消定时器 talkePingOverTimeTimerManager.cancel(); @@ -730,6 +732,8 @@ class StartChartManage { stopCallRequestMessageTimer(); stopSendingRbcuInfoMessages(); stopSendingRbcuProBeMessages(); + stopTalkAcceptTimer(); + stopCallRequestMessageTimer(); // 取消定时器 talkePingOverTimeTimerManager.cancel(); talkDataOverTimeTimerManager.cancel(); diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart index 033c3f57..af05d336 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart @@ -148,7 +148,6 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { ) { // 只允许frameSeq严格递增,乱序或重复帧直接丢弃 if (_lastFrameSeq != null && frameSeq <= _lastFrameSeq!) { - // 可选:打印日志 AppLog.log('丢弃乱序或重复帧: frameSeq=$frameSeq, lastFrameSeq=$_lastFrameSeq'); return; } @@ -162,13 +161,19 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { 'pts': pts, }; + // 如果缓冲区超出最大大小,优先丢弃P/B帧 + while (state.h264FrameBuffer.length >= state.maxFrameBufferSize) { + int pbIndex = state.h264FrameBuffer.indexWhere((f) => + f['frameType'] == TalkDataH264Frame_FrameTypeE.P); + if (pbIndex != -1) { + state.h264FrameBuffer.removeAt(pbIndex); + } else { + state.h264FrameBuffer.removeAt(0); + } + } + // 将帧添加到缓冲区 state.h264FrameBuffer.add(frameMap); - - // 如果缓冲区超出最大大小,移除最早的帧 - while (state.h264FrameBuffer.length > state.maxFrameBufferSize) { - state.h264FrameBuffer.removeAt(0); - } } /// 启动帧处理定时器 @@ -212,16 +217,16 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { int pts = frameMap['pts']; // int pts = DateTime.now().millisecondsSinceEpoch; - // if (frameType == TalkDataH264Frame_FrameTypeE.P) { - // // 以frameSeqI为I帧序号标识 - // if (!(_decodedIFrames.contains(frameSeqI))) { - // AppLog.log('丢弃P帧:未收到对应I帧,frameSeqI=${frameSeqI}'); - // return; - // } - // } else if (frameType == TalkDataH264Frame_FrameTypeE.I) { - // // 记录已解码I帧序号 - // _decodedIFrames.add(frameSeq); - // } + if (frameType == TalkDataH264Frame_FrameTypeE.P) { + // 以frameSeqI为I帧序号标识 + if (!(_decodedIFrames.contains(frameSeqI))) { + AppLog.log('丢弃P帧:未收到对应I帧,frameSeqI=${frameSeqI}'); + return; + } + } else if (frameType == TalkDataH264Frame_FrameTypeE.I) { + // 记录已解码I帧序号 + _decodedIFrames.add(frameSeq); + } // 实时写入h264文件 // _appendH264FrameToFile(frameData, frameType); diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_state.dart b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart index 8703012d..b63ffc29 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_state.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart @@ -109,7 +109,7 @@ class TalkViewNativeDecodeState { // H264帧缓冲区相关 final List> h264FrameBuffer = >[]; // H264帧缓冲区,存储帧数据和类型 - final int maxFrameBufferSize = 7; // 最大缓冲区大小 + final int maxFrameBufferSize = 15; // 最大缓冲区大小 final int targetFps = 30; // 目标解码帧率,只是为了快速填充native的缓冲区 Timer? frameProcessTimer; // 帧处理定时器 bool isProcessingFrame = false; // 是否正在处理帧 From 4de271603d706c412e02ba59efad622a8ba7e462 Mon Sep 17 00:00:00 2001 From: liyi Date: Wed, 14 May 2025 14:53:30 +0800 Subject: [PATCH 098/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E5=B8=B8?= =?UTF-8?q?=E5=BC=80=E6=A8=A1=E5=BC=8F=E8=93=9D=E7=89=99=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E5=8F=91=E9=80=81=E9=A1=BA=E5=BA=8F=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lockSet/normallyOpenMode/normallyOpenMode_page.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/lib/main/lockDetail/lockSet/normallyOpenMode/normallyOpenMode_page.dart b/lib/main/lockDetail/lockSet/normallyOpenMode/normallyOpenMode_page.dart index ed696552..e203a907 100755 --- a/lib/main/lockDetail/lockSet/normallyOpenMode/normallyOpenMode_page.dart +++ b/lib/main/lockDetail/lockSet/normallyOpenMode/normallyOpenMode_page.dart @@ -118,6 +118,15 @@ class _NormallyOpenModePageState extends State with RouteA : SubmitBtn( btnName: '保存'.tr, onClick: () { + if (state.weekDays.value.isEmpty) { + logic.showToast('请选择常开日期'.tr); + return; + } + + if (state.endTimeMinute.value < state.beginTimeMinute.value) { + logic.showToast('结束时间不能小于开始时间哦'.tr); + return; + } logic.sendAutoLock(); }), )), From e754d008c5007ea7b161f3c77a39550e011b0c2d Mon Sep 17 00:00:00 2001 From: liyi Date: Thu, 15 May 2025 16:45:56 +0800 Subject: [PATCH 099/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E5=9B=BE?= =?UTF-8?q?=E4=BC=A0=E6=A0=87=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../views/imageTransmission/image_transmission_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/talk/starChart/views/imageTransmission/image_transmission_page.dart b/lib/talk/starChart/views/imageTransmission/image_transmission_page.dart index c7ce2678..3340151c 100644 --- a/lib/talk/starChart/views/imageTransmission/image_transmission_page.dart +++ b/lib/talk/starChart/views/imageTransmission/image_transmission_page.dart @@ -58,7 +58,7 @@ class _ImageTransmissionPageState extends State backgroundColor: AppColors.mainBackgroundColor, resizeToAvoidBottomInset: false, appBar: TitleAppBar( - barTitle: '图传全自动'.tr, + barTitle: '图传'.tr, haveBack: true, backgroundColor: AppColors.mainColor, backAction: (){ From 069ef1b5924e88b8ef34381ae922cbb1b213b41f Mon Sep 17 00:00:00 2001 From: liyi Date: Thu, 15 May 2025 16:46:14 +0800 Subject: [PATCH 100/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E8=BF=9C?= =?UTF-8?q?=E7=A8=8B=E5=BC=80=E9=94=81=E8=8E=B7=E5=8F=96lockId=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../image_transmission_logic.dart | 58 +++++++++++-------- .../views/talkView/talk_view_logic.dart | 58 +++++++++++-------- 2 files changed, 66 insertions(+), 50 deletions(-) diff --git a/lib/talk/starChart/views/imageTransmission/image_transmission_logic.dart b/lib/talk/starChart/views/imageTransmission/image_transmission_logic.dart index 8cc6f854..c9c42baf 100644 --- a/lib/talk/starChart/views/imageTransmission/image_transmission_logic.dart +++ b/lib/talk/starChart/views/imageTransmission/image_transmission_logic.dart @@ -32,6 +32,8 @@ import 'package:star_lock/talk/starChart/views/imageTransmission/image_transmiss import 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart'; import 'package:star_lock/tools/G711Tool.dart'; import 'package:star_lock/tools/bugly/bugly_tool.dart'; +import 'package:star_lock/tools/commonDataManage.dart'; +import 'package:star_lock/tools/storage.dart'; import '../../../../tools/baseGetXController.dart'; @@ -517,34 +519,40 @@ class ImageTransmissionLogic extends BaseGetXController { // 远程开锁 Future remoteOpenLock() async { + final LockListInfoItemEntity currentKeyInfo = + CommonDataManage().currentKeyInfo; + + var lockId = currentKeyInfo.lockId ?? 0; + var remoteUnlock = currentKeyInfo.lockSetting?.remoteUnlock ?? 0; + final lockPeerId = StartChartManage().lockPeerId; - final lockListPeerId = StartChartManage().lockListPeerId; - int lockId = lockDetailState.keyInfos.value.lockId ?? 0; - - // 如果锁列表获取到peerId,代表有多个锁,使用锁列表的peerId - // 从列表中遍历出对应的peerId - lockListPeerId.forEach((element) { - if (element.network?.peerId == lockPeerId) { - lockId = element.lockId ?? 0; - } - }); - - final LockSetInfoEntity lockSetInfoEntity = - await ApiRepository.to.getLockSettingInfoData( - lockId: lockId.toString(), - ); - if (lockSetInfoEntity.errorCode!.codeIsSuccessful) { - if (lockSetInfoEntity.data?.lockFeature?.remoteUnlock == 1 && - lockSetInfoEntity.data?.lockSettingInfo?.remoteUnlock == 1) { - final LoginEntity entity = await ApiRepository.to - .remoteOpenLock(lockId: lockId.toString(), timeOut: 60); - if (entity.errorCode!.codeIsSuccessful) { - showToast('已开锁'.tr); - StartChartManage().lockListPeerId = []; + final LockListInfoGroupEntity? lockListInfoGroupEntity = + await Storage.getLockMainListData(); + if (lockListInfoGroupEntity != null) { + lockListInfoGroupEntity!.groupList?.forEach((element) { + final lockList = element.lockList; + if (lockList != null && lockList.length != 0) { + for (var lockInfo in lockList) { + final peerId = lockInfo.network?.peerId; + if (peerId != null && peerId != '') { + if (peerId == lockPeerId) { + lockId = lockInfo.lockId ?? 0; + remoteUnlock = lockInfo.lockSetting?.remoteUnlock ?? 0; + } + } + } } - } else { - showToast('该锁的远程开锁功能未启用'.tr); + }); + } + if (remoteUnlock == 1) { + final LoginEntity entity = await ApiRepository.to + .remoteOpenLock(lockId: lockId.toString(), timeOut: 60); + if (entity.errorCode!.codeIsSuccessful) { + showToast('已开锁'.tr); + StartChartManage().lockListPeerId = []; } + } else { + showToast('该锁的远程开锁功能未启用'.tr); } } diff --git a/lib/talk/starChart/views/talkView/talk_view_logic.dart b/lib/talk/starChart/views/talkView/talk_view_logic.dart index 02ed3f25..cfe0d6f2 100644 --- a/lib/talk/starChart/views/talkView/talk_view_logic.dart +++ b/lib/talk/starChart/views/talkView/talk_view_logic.dart @@ -31,6 +31,8 @@ import 'package:star_lock/talk/starChart/star_chart_manage.dart'; import 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart'; import 'package:star_lock/tools/G711Tool.dart'; import 'package:star_lock/tools/bugly/bugly_tool.dart'; +import 'package:star_lock/tools/commonDataManage.dart'; +import 'package:star_lock/tools/storage.dart'; import '../../../../tools/baseGetXController.dart'; @@ -514,34 +516,40 @@ class TalkViewLogic extends BaseGetXController { // 远程开锁 Future remoteOpenLock() async { + final LockListInfoItemEntity currentKeyInfo = + CommonDataManage().currentKeyInfo; + + var lockId = currentKeyInfo.lockId ?? 0; + var remoteUnlock = currentKeyInfo.lockSetting?.remoteUnlock ?? 0; + final lockPeerId = StartChartManage().lockPeerId; - final lockListPeerId = StartChartManage().lockListPeerId; - int lockId = lockDetailState.keyInfos.value.lockId ?? 0; - - // 如果锁列表获取到peerId,代表有多个锁,使用锁列表的peerId - // 从列表中遍历出对应的peerId - lockListPeerId.forEach((element) { - if (element.network?.peerId == lockPeerId) { - lockId = element.lockId ?? 0; - } - }); - - final LockSetInfoEntity lockSetInfoEntity = - await ApiRepository.to.getLockSettingInfoData( - lockId: lockId.toString(), - ); - if (lockSetInfoEntity.errorCode!.codeIsSuccessful) { - if (lockSetInfoEntity.data?.lockFeature?.remoteUnlock == 1 && - lockSetInfoEntity.data?.lockSettingInfo?.remoteUnlock == 1) { - final LoginEntity entity = await ApiRepository.to - .remoteOpenLock(lockId: lockId.toString(), timeOut: 60); - if (entity.errorCode!.codeIsSuccessful) { - showToast('已开锁'.tr); - StartChartManage().lockListPeerId = []; + final LockListInfoGroupEntity? lockListInfoGroupEntity = + await Storage.getLockMainListData(); + if (lockListInfoGroupEntity != null) { + lockListInfoGroupEntity!.groupList?.forEach((element) { + final lockList = element.lockList; + if (lockList != null && lockList.length != 0) { + for (var lockInfo in lockList) { + final peerId = lockInfo.network?.peerId; + if (peerId != null && peerId != '') { + if (peerId == lockPeerId) { + lockId = lockInfo.lockId ?? 0; + remoteUnlock = lockInfo.lockSetting?.remoteUnlock ?? 0; + } + } + } } - } else { - showToast('该锁的远程开锁功能未启用'.tr); + }); + } + if (remoteUnlock == 1) { + final LoginEntity entity = await ApiRepository.to + .remoteOpenLock(lockId: lockId.toString(), timeOut: 60); + if (entity.errorCode!.codeIsSuccessful) { + showToast('已开锁'.tr); + StartChartManage().lockListPeerId = []; } + } else { + showToast('该锁的远程开锁功能未启用'.tr); } } From 90f94e1a9ac2eb5d878749f3da21bd534923cfb1 Mon Sep 17 00:00:00 2001 From: liyi Date: Thu, 15 May 2025 16:46:50 +0800 Subject: [PATCH 101/151] =?UTF-8?q?fix:=E5=AE=8C=E6=88=90=E5=88=87?= =?UTF-8?q?=E6=8D=A2=E6=B8=85=E6=99=B0=E5=BA=A6=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../native/talk_view_native_decode_logic.dart | 257 ++++++++++++++---- .../native/talk_view_native_decode_page.dart | 122 ++++++--- .../native/talk_view_native_decode_state.dart | 3 + 3 files changed, 291 insertions(+), 91 deletions(-) diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart index af05d336..606ae73c 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart @@ -35,6 +35,8 @@ import 'package:star_lock/talk/starChart/views/native/talk_view_native_decode_st import 'package:star_lock/talk/starChart/views/talkView/talk_view_state.dart'; import 'package:star_lock/tools/G711Tool.dart'; import 'package:star_lock/tools/bugly/bugly_tool.dart'; +import 'package:star_lock/tools/commonDataManage.dart'; +import 'package:star_lock/tools/storage.dart'; import 'package:video_decode_plugin/video_decode_plugin.dart'; import '../../../../tools/baseGetXController.dart'; @@ -75,6 +77,16 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { // 新增:记录上一个已接收的frameSeq int? _lastFrameSeq; + // 新增:frameSeq回绕检测标志 + bool _pendingStreamReset = false; + + // 新增:记录切换时的宽高参数 + int _pendingResetWidth = 864; + int _pendingResetHeight = 480; + + // 新增:等待新I帧状态 + bool _waitingForIFrame = false; + // 初始化视频解码器 Future _initVideoDecoder() async { try { @@ -89,12 +101,12 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { // 初始化解码器并获取textureId final textureId = await VideoDecodePlugin.initDecoder(config); if (textureId != null) { - state.textureId.value = textureId; + Future.microtask(() => state.textureId.value = textureId); AppLog.log('视频解码器初始化成功:textureId=$textureId'); - VideoDecodePlugin.setOnFrameRenderedListener((textureId) { - state.isLoading.value = false; AppLog.log('已经开始渲染======='); + // 只有真正渲染出首帧时才关闭loading + Future.microtask(() => state.isLoading.value = false); }); } else { AppLog.log('视频解码器初始化失败'); @@ -146,12 +158,53 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { int frameSeq, int frameSeqI, ) { - // 只允许frameSeq严格递增,乱序或重复帧直接丢弃 - if (_lastFrameSeq != null && frameSeq <= _lastFrameSeq!) { - AppLog.log('丢弃乱序或重复帧: frameSeq=$frameSeq, lastFrameSeq=$_lastFrameSeq'); - return; + // 检测frameSeq回绕,且为I帧 + if (!_pendingStreamReset && + _lastFrameSeq != null && + frameType == TalkDataH264Frame_FrameTypeE.I && + frameSeq < _lastFrameSeq!) { + // 检测到新流I帧,进入loading并重置所有本地状态 + AppLog.log( + '检测到新流I帧,frameSeq回绕,进入loading并重置: frameSeq=$frameSeq, lastFrameSeq=$_lastFrameSeq'); + Future.microtask(() => state.isLoading.value = true); + _pendingStreamReset = true; + // 先暂停帧处理定时器,防止竞态 + _stopFrameProcessTimer(); + // 先释放并重新初始化解码器 + _resetDecoderForNewStream(_pendingResetWidth, _pendingResetHeight); + // 重置所有本地状态 + _lastFrameSeq = null; + _decodedIFrames.clear(); + state.h264FrameBuffer.clear(); + // 再恢复帧处理定时器 + _startFrameProcessTimer(); + // 不return,直接用该I帧初始化解码器并解码 + // 继续往下执行 + } + // 如果处于pendingStreamReset,等待新I帧 + if (_pendingStreamReset) { + if (frameType == TalkDataH264Frame_FrameTypeE.I) { + // 收到新流I帧,关闭loading,恢复正常解码 + AppLog.log('收到新流I帧,关闭loading: frameSeq=$frameSeq'); + //Future.microtask(() => state.isLoading.value = false); + _pendingStreamReset = false; + _lastFrameSeq = frameSeq; + _decodedIFrames.clear(); + _decodedIFrames.add(frameSeq); + // 继续往下执行,直接用该I帧解码 + } else { + // 等待新流I帧期间,丢弃所有非I帧 + AppLog.log('等待新流I帧,丢弃非I帧: frameSeq=$frameSeq, frameType=$frameType'); + return; + } + } else { + // 正常流程 + if (_lastFrameSeq != null && frameSeq <= _lastFrameSeq!) { + AppLog.log('丢弃乱序或重复帧: frameSeq=$frameSeq, lastFrameSeq=$_lastFrameSeq'); + return; + } + _lastFrameSeq = frameSeq; } - _lastFrameSeq = frameSeq; // 创建包含帧数据和类型的Map final Map frameMap = { 'frameData': frameData, @@ -163,8 +216,8 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { // 如果缓冲区超出最大大小,优先丢弃P/B帧 while (state.h264FrameBuffer.length >= state.maxFrameBufferSize) { - int pbIndex = state.h264FrameBuffer.indexWhere((f) => - f['frameType'] == TalkDataH264Frame_FrameTypeE.P); + int pbIndex = state.h264FrameBuffer + .indexWhere((f) => f['frameType'] == TalkDataH264Frame_FrameTypeE.P); if (pbIndex != -1) { state.h264FrameBuffer.removeAt(pbIndex); } else { @@ -209,29 +262,31 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { try { // 取出最早的帧 - final Map frameMap = state.h264FrameBuffer.removeAt(0); - final List frameData = frameMap['frameData']; - final TalkDataH264Frame_FrameTypeE frameType = frameMap['frameType']; - final int frameSeq = frameMap['frameSeq']; - final int frameSeqI = frameMap['frameSeqI']; - int pts = frameMap['pts']; - // int pts = DateTime.now().millisecondsSinceEpoch; - - if (frameType == TalkDataH264Frame_FrameTypeE.P) { - // 以frameSeqI为I帧序号标识 - if (!(_decodedIFrames.contains(frameSeqI))) { - AppLog.log('丢弃P帧:未收到对应I帧,frameSeqI=${frameSeqI}'); - return; - } - } else if (frameType == TalkDataH264Frame_FrameTypeE.I) { - // 记录已解码I帧序号 - _decodedIFrames.add(frameSeq); + final Map? frameMap = state.h264FrameBuffer.isNotEmpty + ? state.h264FrameBuffer.removeAt(0) + : null; + if (frameMap == null) { + state.isProcessingFrame = false; + return; + } + final List? frameData = frameMap['frameData']; + final TalkDataH264Frame_FrameTypeE? frameType = frameMap['frameType']; + final int? frameSeq = frameMap['frameSeq']; + final int? frameSeqI = frameMap['frameSeqI']; + final int? pts = frameMap['pts']; + if (frameData == null || + frameType == null || + frameSeq == null || + frameSeqI == null || + pts == null) { + state.isProcessingFrame = false; + return; + } + // 解码器未初始化或textureId为null时跳过 + if (state.textureId.value == null) { + state.isProcessingFrame = false; + return; } - // 实时写入h264文件 - // _appendH264FrameToFile(frameData, frameType); - - // final timestamp = DateTime.now().millisecondsSinceEpoch; - // final timestamp64 = timestamp is int ? timestamp : timestamp.toInt(); await VideoDecodePlugin.sendFrame( frameData: frameData, frameType: frameType == TalkDataH264Frame_FrameTypeE.I ? 0 : 1, @@ -462,6 +517,7 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { // 初始化视频解码器 _initVideoDecoder(); + _initHdOptions(); // 初始化H264帧缓冲区 state.h264FrameBuffer.clear(); state.isProcessingFrame = false; @@ -490,7 +546,7 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { // 释放视频解码器资源 if (state.textureId.value != null) { VideoDecodePlugin.releaseDecoder(); - state.textureId.value = null; + Future.microtask(() => state.textureId.value = null); } // 取消数据流监听 @@ -577,36 +633,42 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { } } - // 远程开锁 +// 远程开锁 Future remoteOpenLock() async { + final LockListInfoItemEntity currentKeyInfo = + CommonDataManage().currentKeyInfo; + + var lockId = currentKeyInfo.lockId ?? 0; + var remoteUnlock = currentKeyInfo.lockSetting?.remoteUnlock ?? 0; + final lockPeerId = StartChartManage().lockPeerId; - final lockListPeerId = StartChartManage().lockListPeerId; - int lockId = lockDetailState.keyInfos.value.lockId ?? 0; - - // 如果锁列表获取到peerId,代表有多个锁,使用锁列表的peerId - // 从列表中遍历出对应的peerId - lockListPeerId.forEach((element) { - if (element.network?.peerId == lockPeerId) { - lockId = element.lockId ?? 0; - } - }); - - final LockSetInfoEntity lockSetInfoEntity = - await ApiRepository.to.getLockSettingInfoData( - lockId: lockId.toString(), - ); - if (lockSetInfoEntity.errorCode!.codeIsSuccessful) { - if (lockSetInfoEntity.data?.lockFeature?.remoteUnlock == 1 && - lockSetInfoEntity.data?.lockSettingInfo?.remoteUnlock == 1) { - final LoginEntity entity = await ApiRepository.to - .remoteOpenLock(lockId: lockId.toString(), timeOut: 60); - if (entity.errorCode!.codeIsSuccessful) { - showToast('已开锁'.tr); - StartChartManage().lockListPeerId = []; + final LockListInfoGroupEntity? lockListInfoGroupEntity = + await Storage.getLockMainListData(); + if (lockListInfoGroupEntity != null) { + lockListInfoGroupEntity!.groupList?.forEach((element) { + final lockList = element.lockList; + if (lockList != null && lockList.length != 0) { + for (var lockInfo in lockList) { + final peerId = lockInfo.network?.peerId; + if (peerId != null && peerId != '') { + if (peerId == lockPeerId) { + lockId = lockInfo.lockId ?? 0; + remoteUnlock = lockInfo.lockSetting?.remoteUnlock ?? 0; + } + } + } } - } else { - showToast('该锁的远程开锁功能未启用'.tr); + }); + } + if (remoteUnlock == 1) { + final LoginEntity entity = await ApiRepository.to + .remoteOpenLock(lockId: lockId.toString(), timeOut: 60); + if (entity.errorCode!.codeIsSuccessful) { + showToast('已开锁'.tr); + StartChartManage().lockListPeerId = []; } + } else { + showToast('该锁的远程开锁功能未启用'.tr); } } @@ -1172,4 +1234,81 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { } } } + + // 切换清晰度的方法,后续补充具体实现 + void onQualityChanged(String quality) async { + state.currentQuality.value = quality; + TalkExpectReq talkExpectReq = StartChartManage().getDefaultTalkExpect(); + final audioType = talkExpectReq.audioType; + int width = 864; + int height = 480; + switch (quality) { + case '高清': + talkExpectReq = TalkExpectReq( + videoType: [VideoTypeE.H264_720P], + audioType: audioType, + ); + width = 1280; + height = 720; + break; + case '标清': + talkExpectReq = TalkExpectReq( + videoType: [VideoTypeE.H264], + audioType: audioType, + ); + width = 864; + height = 480; + break; + } + + /// 修改发送预期数据 + StartChartManage().changeTalkExpectDataTypeAndReStartTalkExpectMessageTimer( + talkExpect: talkExpectReq); + + // 不立即loading,继续解码旧流帧,等待frameSeq回绕检测 + // 仅重置frameSeq回绕检测标志 + _pendingStreamReset = false; + _pendingResetWidth = width; + _pendingResetHeight = height; + } + + void _initHdOptions() { + TalkExpectReq talkExpectReq = StartChartManage().getDefaultTalkExpect(); + final videoType = talkExpectReq.videoType; + if (videoType.contains(VideoTypeE.H264)) { + state.currentQuality.value = '标清'; + } else if (videoType.contains(VideoTypeE.H264_720P)) { + state.currentQuality.value = '高清'; + } + } + + // 新增:重置解码器方法 + Future _resetDecoderForNewStream(int width, int height) async { + try { + if (state.textureId.value != null) { + await VideoDecodePlugin.releaseDecoder(); + Future.microtask(() => state.textureId.value = null); + } + final config = VideoDecoderConfig( + width: width, + height: height, + codecType: 'h264', + ); + final textureId = await VideoDecodePlugin.initDecoder(config); + if (textureId != null) { + Future.microtask(() => state.textureId.value = textureId); + AppLog.log('frameSeq回绕后解码器初始化成功:textureId=$textureId'); + VideoDecodePlugin.setOnFrameRenderedListener((textureId) { + AppLog.log('已经开始渲染======='); + // 只有真正渲染出首帧时才关闭loading + Future.microtask(() => state.isLoading.value = false); + }); + } else { + AppLog.log('frameSeq回绕后解码器初始化失败'); + } + _startFrameProcessTimer(); + } catch (e) { + AppLog.log('frameSeq回绕时解码器初始化错误: $e'); + } + } } diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_page.dart b/lib/talk/starChart/views/native/talk_view_native_decode_page.dart index b7c1fb12..f4a7eb13 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_page.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_page.dart @@ -97,40 +97,42 @@ class _TalkViewNativeDecodePageState extends State final double scaleWidth = physicalWidth / rotatedImageWidth; final double scaleHeight = physicalHeight / rotatedImageHeight; max(scaleWidth, scaleHeight); // 选择较大的缩放比例 - return state.isLoading.isTrue - ? Image.asset( - 'images/main/monitorBg.png', - width: screenWidth, - height: screenHeight, - fit: BoxFit.cover, - ) - : Positioned.fill( - child: PopScope( - canPop: false, - child: RepaintBoundary( - key: state.globalKey, - child: SizedBox.expand( - child: RotatedBox( - // 解码器不支持硬件旋转,使用RotatedBox - quarterTurns: - startChartManage.rotateAngle ~/ 90, - child: Platform.isIOS - ? Transform.scale( - scale: 1.008, // 轻微放大,消除iOS白边 - child: Texture( - textureId: state.textureId.value!, - filterQuality: FilterQuality.medium, - ), - ) - : Texture( - textureId: state.textureId.value!, - filterQuality: FilterQuality.medium, - ), - ), - ), + // 防御性处理:只要loading中或textureId为null,优先渲染loading/占位 + if (state.isLoading.isTrue || state.textureId.value == null) { + return Image.asset( + 'images/main/monitorBg.png', + width: screenWidth, + height: screenHeight, + fit: BoxFit.cover, + ); + } else { + return Positioned.fill( + child: PopScope( + canPop: false, + child: RepaintBoundary( + key: state.globalKey, + child: SizedBox.expand( + child: RotatedBox( + // 解码器不支持硬件旋转,使用RotatedBox + quarterTurns: startChartManage.rotateAngle ~/ 90, + child: Platform.isIOS + ? Transform.scale( + scale: 1.008, // 轻微放大,消除iOS白边 + child: Texture( + textureId: state.textureId.value!, + filterQuality: FilterQuality.medium, + ), + ) + : Texture( + textureId: state.textureId.value!, + filterQuality: FilterQuality.medium, + ), ), ), - ); + ), + ), + ); + } }, ), @@ -295,6 +297,62 @@ class _TalkViewNativeDecodePageState extends State ), ), ), + SizedBox(width: 50.w), + // 清晰度切换按钮 + GestureDetector( + onTap: () async { + // 弹出底部弹出层,选择清晰度 + showModalBottomSheet( + context: context, + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20.w)), + ), + builder: (BuildContext context) { + final List qualities = ['高清', '标清']; + return SafeArea( + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: qualities.map((q) { + return Obx(() => InkWell( + onTap: () { + Navigator.of(context).pop(); + logic.onQualityChanged(q); + }, + child: Container( + padding: EdgeInsets.symmetric(vertical: 18.w), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Text( + q, + style: TextStyle( + color: state.currentQuality.value == q + ? AppColors.mainColor + : Colors.black, + fontWeight: state.currentQuality.value == q + ? FontWeight.bold + : FontWeight.normal, + fontSize: 28.sp, + ), + ), + ], + ), + ), + )); + }).toList(), + ), + ), + ); + }, + ); + }, + child: Container( + child: Icon(Icons.high_quality_outlined, color: Colors.white, size: 38.w), + ), + ), ]); } diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_state.dart b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart index b63ffc29..8d176500 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_state.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_state.dart @@ -117,4 +117,7 @@ class TalkViewNativeDecodeState { // H264文件保存相关 String? h264FilePath; File? h264File; + + // 当前清晰度选项,初始为'高清' + RxString currentQuality = '高清'.obs; // 可选:高清、标清、流畅 } From e2f8400ddcc8bdf70caaeb763daad2cee7875771 Mon Sep 17 00:00:00 2001 From: liyi Date: Thu, 15 May 2025 16:47:08 +0800 Subject: [PATCH 102/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E5=AF=B9?= =?UTF-8?q?=E8=AE=B2=E6=94=AF=E6=8C=81=E9=A1=B9=E5=88=A4=E6=96=AD=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handle/impl/udp_talk_request_handler.dart | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart index 7ce19cf6..5b72e229 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart @@ -59,8 +59,11 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle // 收到对讲请求的应答 startChartManage.FromPeerId = scpMessage.ToPeerId!; startChartManage.ToPeerId = scpMessage.FromPeerId!; + startChartManage.lockPeerId = scpMessage.FromPeerId!; // 处理预期数据格式 - _handleResponseSendExpect(); + _handleResponseSendExpect( + lockPeerID: scpMessage.FromPeerId!, + ); // 发送预期数据 startChartManage.startTalkExpectTimer(); // 停止发送对讲请求 @@ -247,12 +250,33 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle } /// app主动发请求,收到回复后发送的预期数据 - void _handleResponseSendExpect() { + void _handleResponseSendExpect({ + required String lockPeerID, + }) async { final LockListInfoItemEntity currentKeyInfo = CommonDataManage().currentKeyInfo; - final isH264 = currentKeyInfo.lockFeature?.isH264 == 1; - final isMJpeg = currentKeyInfo.lockFeature?.isMJpeg == 1; + var isH264 = currentKeyInfo.lockFeature?.isH264 == 1; + var isMJpeg = currentKeyInfo.lockFeature?.isMJpeg == 1; + + final LockListInfoGroupEntity? lockListInfoGroupEntity = + await Storage.getLockMainListData(); + if (lockListInfoGroupEntity != null) { + lockListInfoGroupEntity!.groupList?.forEach((element) { + final lockList = element.lockList; + if (lockList != null && lockList.length != 0) { + for (var lockInfo in lockList) { + final peerId = lockInfo.network?.peerId; + if (peerId != null && peerId != '') { + if (peerId == lockPeerID) { + isH264 = lockInfo.lockFeature?.isH264 == 1; + isMJpeg = lockInfo.lockFeature?.isMJpeg == 1; + } + } + } + } + }); + } // 优先使用H264,其次是MJPEG if (isH264) { // 锁支持H264,发送H264视频和G711音频期望 From 17e9c0e5ed26dca4ce05f638cba8f5b3824279be Mon Sep 17 00:00:00 2001 From: liyi Date: Thu, 15 May 2025 16:48:50 +0800 Subject: [PATCH 103/151] =?UTF-8?q?fix:=E6=9B=B4=E6=96=B0=E5=AF=B9?= =?UTF-8?q?=E8=AE=B2=E6=8F=92=E4=BB=B6=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 68d2d366..36b08c64 100755 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -130,7 +130,7 @@ dependencies: video_decode_plugin: git: url: git@code.star-lock.cn:liyi/video_decode_plugin.git - ref: 38df1883f5108ec1ce590ba52318815333fded38 + ref: 68bb4b7fb637ef5a78856908e1bc464f50fe967a flutter_localizations: sdk: flutter From 755ec4965c02c9f9836a489650c23020572cd2d5 Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 16 May 2025 11:42:03 +0800 Subject: [PATCH 104/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/Gemfile | 1 + ios/Gemfile | 1 + 2 files changed, 2 insertions(+) diff --git a/android/Gemfile b/android/Gemfile index cdd3a6b3..5f5b8322 100644 --- a/android/Gemfile +++ b/android/Gemfile @@ -1,6 +1,7 @@ source "https://rubygems.org" gem "fastlane" +gem 'nkf', '0.2.0' plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/ios/Gemfile b/ios/Gemfile index 21cb5dfc..cb7538c4 100644 --- a/ios/Gemfile +++ b/ios/Gemfile @@ -5,3 +5,4 @@ gem 'cocoapods', '1.14.3' gem 'public_suffix', '~> 4.0' plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') eval_gemfile(plugins_path) if File.exist?(plugins_path) +gem 'nkf', '0.2.0' From 7a73356ed208aae12174bc7c498dcf4a597b2bb0 Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 16 May 2025 12:02:34 +0800 Subject: [PATCH 105/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 17a01616..254f67e1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -66,8 +66,9 @@ variables: before_script: - ls -li - export NEXT_VERSION="$(cat app_new.version)" -# - flutter pub get + - bundle config set --local path 'vendor/bundle' - bundle install --gemfile android/Gemfile --quiet + - gem pristine --all || true cache: paths: - app_new.version @@ -77,8 +78,9 @@ variables: before_script: - ls -li - export NEXT_VERSION="$(cat app_new.version)" -# - flutter pub get + - bundle config set --local path 'vendor/bundle' - bundle install --gemfile ios/Gemfile --quiet + - gem pristine --all || true cache: paths: - app_new.version From 47d9c4d2eb3887da355e5b658c9cc887f4a7c263 Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 16 May 2025 15:17:58 +0800 Subject: [PATCH 106/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 254f67e1..a5f0e421 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -64,11 +64,27 @@ variables: .setup_fastlane_android: extends: .build_rule before_script: + - export PATH="$HOME/.rbenv/bin:$PATH" + - eval "$(rbenv init -)" + - rbenv global 2.7.8 + - export PATH="$HOME/.rbenv/shims:$PATH" + - which ruby + - ruby -v + - gem install bundler - ls -li - export NEXT_VERSION="$(cat app_new.version)" - bundle config set --local path 'vendor/bundle' - bundle install --gemfile android/Gemfile --quiet - gem pristine --all || true + script: + - echo "=== DEBUG INFO (android) ===" + - which ruby + - ruby -v + - which gem + - gem -v + - echo $PATH + - env + - bash android/build.sh cache: paths: - app_new.version @@ -76,11 +92,27 @@ variables: .setup_fastlane_ios: extends: .build_rule before_script: + - export PATH="$HOME/.rbenv/bin:$PATH" + - eval "$(rbenv init -)" + - rbenv global 2.7.8 + - export PATH="$HOME/.rbenv/shims:$PATH" + - which ruby + - ruby -v + - gem install bundler - ls -li - export NEXT_VERSION="$(cat app_new.version)" - bundle config set --local path 'vendor/bundle' - bundle install --gemfile ios/Gemfile --quiet - gem pristine --all || true + script: + - echo "=== DEBUG INFO (ios) ===" + - which ruby + - ruby -v + - which gem + - gem -v + - echo $PATH + - env + - bash ios/build.sh cache: paths: - app_new.version From ac0d8073eeb965afb0cd00bf01e40e9b20636953 Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 16 May 2025 15:25:03 +0800 Subject: [PATCH 107/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a5f0e421..3134ae08 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -64,19 +64,24 @@ variables: .setup_fastlane_android: extends: .build_rule before_script: + # 初始化rbenv环境,确保使用用户级Ruby - export PATH="$HOME/.rbenv/bin:$PATH" - eval "$(rbenv init -)" - rbenv global 2.7.8 - export PATH="$HOME/.rbenv/shims:$PATH" - - which ruby - - ruby -v - - gem install bundler + - which ruby # 输出当前使用的ruby路径,便于调试 + - ruby -v # 输出当前ruby版本,便于调试 + # 切换到国内RubyGems镜像源,加速gem下载(如在中国大陆) + - gem sources --add https://gems.ruby-china.com/ --remove https://rubygems.org/ # 如在国外可移除此行 + # 优先使用已存在的bundler,若无则用国内源安装,减少重复安装耗时 + - bundle -v || gem install bundler --source https://gems.ruby-china.com/ - ls -li - export NEXT_VERSION="$(cat app_new.version)" - - bundle config set --local path 'vendor/bundle' + - bundle config set --local path 'vendor/bundle' # 避免全局权限问题,依赖装到本地 - bundle install --gemfile android/Gemfile --quiet - - gem pristine --all || true + - gem pristine --all || true # 修复所有未编译的gem扩展 script: + # 输出调试信息,便于后续排查环境问题 - echo "=== DEBUG INFO (android) ===" - which ruby - ruby -v @@ -92,19 +97,24 @@ variables: .setup_fastlane_ios: extends: .build_rule before_script: + # 初始化rbenv环境,确保使用用户级Ruby - export PATH="$HOME/.rbenv/bin:$PATH" - eval "$(rbenv init -)" - rbenv global 2.7.8 - export PATH="$HOME/.rbenv/shims:$PATH" - - which ruby - - ruby -v - - gem install bundler + - which ruby # 输出当前使用的ruby路径,便于调试 + - ruby -v # 输出当前ruby版本,便于调试 + # 切换到国内RubyGems镜像源,加速gem下载(如在中国大陆) + - gem sources --add https://gems.ruby-china.com/ --remove https://rubygems.org/ # 如在国外可移除此行 + # 优先使用已存在的bundler,若无则用国内源安装,减少重复安装耗时 + - bundle -v || gem install bundler --source https://gems.ruby-china.com/ - ls -li - export NEXT_VERSION="$(cat app_new.version)" - - bundle config set --local path 'vendor/bundle' + - bundle config set --local path 'vendor/bundle' # 避免全局权限问题,依赖装到本地 - bundle install --gemfile ios/Gemfile --quiet - - gem pristine --all || true + - gem pristine --all || true # 修复所有未编译的gem扩展 script: + # 输出调试信息,便于后续排查环境问题 - echo "=== DEBUG INFO (ios) ===" - which ruby - ruby -v From 3ab1e5e4519649e8b7a0d0bc02c35acea9ebe743 Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 16 May 2025 15:43:00 +0800 Subject: [PATCH 108/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/build.sh | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ios/build.sh b/ios/build.sh index a8a977bf..b492d623 100755 --- a/ios/build.sh +++ b/ios/build.sh @@ -10,6 +10,18 @@ cd ${CI_PROJECT_DIR}/ios #bundle exec pod install echo "ENV_BUILD_TAG:${ENV_BUILD_TAG},ENV_BUILD_BRANCH:${ENV_BUILD_BRANCH}" regex='^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]+\.[0-9]+)?$' + +# ==== 调试输出,确认环境和依赖 ==== +echo "=== FASTLANE/GEM/ENV DEBUG ===" +which fastlane +fastlane -v +which bundle +bundle -v +echo $PATH +gem list | grep fastlane +gem list | grep digest-crc +# ==== END DEBUG ==== + if [[ "${ENV_BUILD_BRANCH}" == "canary_release" ]]; then echo "===build canary_release: ${NEXT_VERSION}" export ENV_BUILD_TAG=${NEXT_VERSION} From 3201f839eb6a81a4dc5bb538bdac001e4605cd59 Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 16 May 2025 16:00:39 +0800 Subject: [PATCH 109/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3134ae08..d29c133a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -64,7 +64,7 @@ variables: .setup_fastlane_android: extends: .build_rule before_script: - # 初始化rbenv环境,确保使用用户级Ruby + - rm -rf ~/.gem ~/.bundle vendor/bundle # 清理依赖缓存,防止环境污染 - export PATH="$HOME/.rbenv/bin:$PATH" - eval "$(rbenv init -)" - rbenv global 2.7.8 @@ -97,7 +97,7 @@ variables: .setup_fastlane_ios: extends: .build_rule before_script: - # 初始化rbenv环境,确保使用用户级Ruby + - rm -rf ~/.gem ~/.bundle vendor/bundle # 清理依赖缓存,防止环境污染 - export PATH="$HOME/.rbenv/bin:$PATH" - eval "$(rbenv init -)" - rbenv global 2.7.8 @@ -161,7 +161,15 @@ generate_next_version: build_android: stage: build-artifacts extends: .setup_fastlane_android - script: bash android/build.sh + script: + - echo "=== DEBUG INFO (android) ===" + - which ruby + - ruby -v + - which gem + - gem -v + - echo $PATH + - env + - bash android/build.sh artifacts: paths: - build/app/outputs/flutter-apk/ @@ -169,7 +177,15 @@ build_android: build_ios: stage: build-artifacts extends: .setup_fastlane_ios + needs: [build_android] # 确保build_ios在build_android完成后再执行 script: + - echo "=== DEBUG INFO (ios) ===" + - which ruby + - ruby -v + - which gem + - gem -v + - echo $PATH + - env - bash ios/build.sh artifacts: paths: From 448268ecc7d404d1963a8b89839157358741e436 Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 16 May 2025 16:21:41 +0800 Subject: [PATCH 110/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d29c133a..2b1ce4bb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -64,21 +64,19 @@ variables: .setup_fastlane_android: extends: .build_rule before_script: - - rm -rf ~/.gem ~/.bundle vendor/bundle # 清理依赖缓存,防止环境污染 + - rm -rf ~/.gem ~/.bundle vendor/bundle_android # 强烈建议每次清理,防止并发/缓存污染 - export PATH="$HOME/.rbenv/bin:$PATH" - eval "$(rbenv init -)" - rbenv global 2.7.8 - export PATH="$HOME/.rbenv/shims:$PATH" - which ruby # 输出当前使用的ruby路径,便于调试 - ruby -v # 输出当前ruby版本,便于调试 - # 切换到国内RubyGems镜像源,加速gem下载(如在中国大陆) - gem sources --add https://gems.ruby-china.com/ --remove https://rubygems.org/ # 如在国外可移除此行 - # 优先使用已存在的bundler,若无则用国内源安装,减少重复安装耗时 - bundle -v || gem install bundler --source https://gems.ruby-china.com/ - ls -li - export NEXT_VERSION="$(cat app_new.version)" - - bundle config set --local path 'vendor/bundle' # 避免全局权限问题,依赖装到本地 - - bundle install --gemfile android/Gemfile --quiet + - bundle config set --local path 'vendor/bundle_android' # Android独立依赖目录 + - bundle install --gemfile android/Gemfile # 去掉--quiet,便于观察进度 - gem pristine --all || true # 修复所有未编译的gem扩展 script: # 输出调试信息,便于后续排查环境问题 @@ -97,21 +95,19 @@ variables: .setup_fastlane_ios: extends: .build_rule before_script: - - rm -rf ~/.gem ~/.bundle vendor/bundle # 清理依赖缓存,防止环境污染 + - rm -rf ~/.gem ~/.bundle vendor/bundle_ios # 强烈建议每次清理,防止并发/缓存污染 - export PATH="$HOME/.rbenv/bin:$PATH" - eval "$(rbenv init -)" - rbenv global 2.7.8 - export PATH="$HOME/.rbenv/shims:$PATH" - which ruby # 输出当前使用的ruby路径,便于调试 - ruby -v # 输出当前ruby版本,便于调试 - # 切换到国内RubyGems镜像源,加速gem下载(如在中国大陆) - gem sources --add https://gems.ruby-china.com/ --remove https://rubygems.org/ # 如在国外可移除此行 - # 优先使用已存在的bundler,若无则用国内源安装,减少重复安装耗时 - bundle -v || gem install bundler --source https://gems.ruby-china.com/ - ls -li - export NEXT_VERSION="$(cat app_new.version)" - - bundle config set --local path 'vendor/bundle' # 避免全局权限问题,依赖装到本地 - - bundle install --gemfile ios/Gemfile --quiet + - bundle config set --local path 'vendor/bundle_ios' # iOS独立依赖目录 + - bundle install --gemfile ios/Gemfile # 去掉--quiet,便于观察进度 - gem pristine --all || true # 修复所有未编译的gem扩展 script: # 输出调试信息,便于后续排查环境问题 From a053e238437ef9b82a7ea21282c89d01bdf5dea8 Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 16 May 2025 16:25:38 +0800 Subject: [PATCH 111/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2b1ce4bb..835d30d9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -173,7 +173,6 @@ build_android: build_ios: stage: build-artifacts extends: .setup_fastlane_ios - needs: [build_android] # 确保build_ios在build_android完成后再执行 script: - echo "=== DEBUG INFO (ios) ===" - which ruby From faca7eae3ec4cc6c6804aa825ad7014727faea4d Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 16 May 2025 16:39:49 +0800 Subject: [PATCH 112/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 2 ++ android/Gemfile | 2 +- ios/Gemfile | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 835d30d9..bc8c4c69 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -72,6 +72,7 @@ variables: - which ruby # 输出当前使用的ruby路径,便于调试 - ruby -v # 输出当前ruby版本,便于调试 - gem sources --add https://gems.ruby-china.com/ --remove https://rubygems.org/ # 如在国外可移除此行 + - bundle config mirror.https://rubygems.org https://gems.ruby-china.com - bundle -v || gem install bundler --source https://gems.ruby-china.com/ - ls -li - export NEXT_VERSION="$(cat app_new.version)" @@ -103,6 +104,7 @@ variables: - which ruby # 输出当前使用的ruby路径,便于调试 - ruby -v # 输出当前ruby版本,便于调试 - gem sources --add https://gems.ruby-china.com/ --remove https://rubygems.org/ # 如在国外可移除此行 + - bundle config mirror.https://rubygems.org https://gems.ruby-china.com - bundle -v || gem install bundler --source https://gems.ruby-china.com/ - ls -li - export NEXT_VERSION="$(cat app_new.version)" diff --git a/android/Gemfile b/android/Gemfile index 5f5b8322..3de6080f 100644 --- a/android/Gemfile +++ b/android/Gemfile @@ -1,4 +1,4 @@ -source "https://rubygems.org" +source "https://gems.ruby-china.com" gem "fastlane" gem 'nkf', '0.2.0' diff --git a/ios/Gemfile b/ios/Gemfile index cb7538c4..8a7082c8 100644 --- a/ios/Gemfile +++ b/ios/Gemfile @@ -1,4 +1,4 @@ -source "https://rubygems.org" +source "https://gems.ruby-china.com" gem "fastlane" gem 'cocoapods', '1.14.3' From 84ca900ddfe0f9bd6628355c3e8d50fe3e066805 Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 16 May 2025 16:45:21 +0800 Subject: [PATCH 113/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 2 +- android/Gemfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bc8c4c69..69738d60 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -72,7 +72,7 @@ variables: - which ruby # 输出当前使用的ruby路径,便于调试 - ruby -v # 输出当前ruby版本,便于调试 - gem sources --add https://gems.ruby-china.com/ --remove https://rubygems.org/ # 如在国外可移除此行 - - bundle config mirror.https://rubygems.org https://gems.ruby-china.com + - bundle config mirror.https://rubygems.org https://mirrors.aliyun.com/rubygems/ - bundle -v || gem install bundler --source https://gems.ruby-china.com/ - ls -li - export NEXT_VERSION="$(cat app_new.version)" diff --git a/android/Gemfile b/android/Gemfile index 3de6080f..79087196 100644 --- a/android/Gemfile +++ b/android/Gemfile @@ -1,4 +1,4 @@ -source "https://gems.ruby-china.com" +source "https://mirrors.aliyun.com/rubygems/" gem "fastlane" gem 'nkf', '0.2.0' From 7d4e2574040094cd1bfbe08a91e95831ab1fcb2f Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 16 May 2025 17:06:49 +0800 Subject: [PATCH 114/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E5=BC=80?= =?UTF-8?q?=E5=85=B3=E9=9D=99=E9=9F=B3=E6=97=B6=E4=BD=BF=E7=94=A8=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E7=9A=84=E6=B8=85=E6=99=B0=E5=BA=A6=E4=BD=9C=E4=B8=BA?= =?UTF-8?q?=E6=9C=9F=E6=9C=9B=E6=95=B0=E6=8D=AE=E5=8F=91=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../views/native/talk_view_native_decode_logic.dart | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart index 606ae73c..9ddf4a57 100644 --- a/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart +++ b/lib/talk/starChart/views/native/talk_view_native_decode_logic.dart @@ -576,17 +576,25 @@ class TalkViewNativeDecodeLogic extends BaseGetXController { /// 更新发送预期数据 void updateTalkExpect() { + // 清晰度与VideoTypeE的映射 + final Map qualityToVideoType = { + '标清': VideoTypeE.H264, + '高清': VideoTypeE.H264_720P, + // 可扩展更多清晰度 + }; TalkExpectReq talkExpectReq = TalkExpectReq(); state.isOpenVoice.value = !state.isOpenVoice.value; + // 根据当前清晰度动态设置videoType + VideoTypeE currentVideoType = qualityToVideoType[state.currentQuality.value] ?? VideoTypeE.H264; if (!state.isOpenVoice.value) { talkExpectReq = TalkExpectReq( - videoType: [VideoTypeE.H264], + videoType: [currentVideoType], audioType: [], ); showToast('已静音'.tr); } else { talkExpectReq = TalkExpectReq( - videoType: [VideoTypeE.H264], + videoType: [currentVideoType], audioType: [AudioTypeE.G711], ); } From 8e122e5a09b338ba48e5e98e8ae8de58d407c1e6 Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 16 May 2025 17:07:05 +0800 Subject: [PATCH 115/151] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0=E5=87=BA?= =?UTF-8?q?=E7=8E=B0=E6=B7=B7mjpeg=E6=9C=9F=E6=9C=9B=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E7=9A=84=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handle/impl/udp_talk_request_handler.dart | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart b/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart index 5b72e229..195c2ab6 100644 --- a/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart +++ b/lib/talk/starChart/handle/impl/udp_talk_request_handler.dart @@ -237,15 +237,18 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle if (isH264) { // 锁支持H264,发送H264视频和G711音频期望 startChartManage.sendOnlyH264VideoTalkExpectData(); - print('app收到的对讲请求后,发送的预期数据=========锁支持H264,发送H264视频格式期望数据'); + print( + 'app收到的对讲请求后,发送的预期数据=========锁支持H264,发送H264视频格式期望数据,peerID=${lockPeerID}'); } else if (isMJpeg) { // 锁只支持MJPEG,发送图像视频和G711音频期望 startChartManage.sendOnlyImageVideoTalkExpectData(); - print('app收到的对讲请求后,发送的预期数据=========锁不支持H264,支持MJPEG,发送MJPEG视频格式期望数据'); + print( + 'app收到的对讲请求后,发送的预期数据=========锁不支持H264,支持MJPEG,发送MJPEG视频格式期望数据,peerID=${lockPeerID}'); } else { // 默认使用图像视频 startChartManage.sendOnlyImageVideoTalkExpectData(); - print('app收到的对讲请求后,发送的预期数据=========锁不支持H264和MJPEG,默认发送图像视频格式期望数据'); + print( + 'app收到的对讲请求后,发送的预期数据=========锁不支持H264和MJPEG,默认发送MJPEG视频格式期望数据,peerID=${lockPeerID}'); } } @@ -281,15 +284,18 @@ class UdpTalkRequestHandler extends ScpMessageBaseHandle if (isH264) { // 锁支持H264,发送H264视频和G711音频期望 startChartManage.sendH264VideoAndG711AudioTalkExpectData(); - print('app主动发请求,收到回复后发送的预期数据=======锁支持H264,发送H264视频格式期望数据'); + AppLog.log( + 'app主动发对讲请求,收到回复后发送的预期数据=======锁支持H264,发送H264视频格式期望数据,peerID=${lockPeerID}'); } else if (isMJpeg) { // 锁只支持MJPEG,发送图像视频和G711音频期望 startChartManage.sendImageVideoAndG711AudioTalkExpectData(); - print('app主动发请求,收到回复后发送的预期数据=======锁不支持H264,支持MJPEG,发送MJPEG视频格式期望数据'); + AppLog.log( + 'app主动发对讲请求,收到回复后发送的预期数据=======锁不支持H264,支持MJPEG,发送MJPEG视频格式期望数据,peerID=${lockPeerID}'); } else { // 默认使用图像视频 startChartManage.sendImageVideoAndG711AudioTalkExpectData(); - print('app主动发请求,收到回复后发送的预期数据=======锁不支持H264和MJPEG,默认发送图像视频格式期望数据'); + AppLog.log( + 'app主动发对讲请求,收到回复后发送的预期数据=======锁不支持H264和MJPEG,默认发送MJPEG视频格式期望数据,peerID=${lockPeerID}'); } } } From e809adf98f8e7a514cb06c999f9fa17525ddcbfe Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 16 May 2025 17:26:15 +0800 Subject: [PATCH 116/151] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0bundle=20install?= =?UTF-8?q?=E9=87=8D=E8=AF=95=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 6 ++--- scripts/bundle_install_retry.sh | 46 +++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 scripts/bundle_install_retry.sh diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 69738d60..4e173a86 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -76,8 +76,7 @@ variables: - bundle -v || gem install bundler --source https://gems.ruby-china.com/ - ls -li - export NEXT_VERSION="$(cat app_new.version)" - - bundle config set --local path 'vendor/bundle_android' # Android独立依赖目录 - - bundle install --gemfile android/Gemfile # 去掉--quiet,便于观察进度 + - bash scripts/bundle_install_retry.sh android/Gemfile vendor/bundle_android - gem pristine --all || true # 修复所有未编译的gem扩展 script: # 输出调试信息,便于后续排查环境问题 @@ -108,8 +107,7 @@ variables: - bundle -v || gem install bundler --source https://gems.ruby-china.com/ - ls -li - export NEXT_VERSION="$(cat app_new.version)" - - bundle config set --local path 'vendor/bundle_ios' # iOS独立依赖目录 - - bundle install --gemfile ios/Gemfile # 去掉--quiet,便于观察进度 + - bash scripts/bundle_install_retry.sh ios/Gemfile vendor/bundle_ios - gem pristine --all || true # 修复所有未编译的gem扩展 script: # 输出调试信息,便于后续排查环境问题 diff --git a/scripts/bundle_install_retry.sh b/scripts/bundle_install_retry.sh new file mode 100644 index 00000000..2678b221 --- /dev/null +++ b/scripts/bundle_install_retry.sh @@ -0,0 +1,46 @@ +#!/bin/bash +# 用法: ./scripts/bundle_install_retry.sh +# 例如: ./scripts/bundle_install_retry.sh android/Gemfile vendor/bundle_android + +GEMFILE_PATH="$1" +BUNDLE_PATH="$2" + +if [ -z "$GEMFILE_PATH" ] || [ -z "$BUNDLE_PATH" ]; then + echo "用法: $0 " + exit 1 +fi + +try_count=0 +max_try=3 +success=0 + +# 先用阿里云镜像 +bundle config mirror.https://rubygems.org https://mirrors.aliyun.com/rubygems/ +while [ $try_count -lt $max_try ]; do + echo "[INFO] 第$((try_count+1))次尝试使用阿里云镜像 bundle install..." + bundle config set --local path "$BUNDLE_PATH" + bundle install --gemfile "$GEMFILE_PATH" + if [ $? -eq 0 ]; then + echo "[SUCCESS] 使用阿里云镜像 bundle install 成功" + success=1 + break + fi + try_count=$((try_count+1)) + sleep 2 + echo "[WARN] 阿里云镜像 bundle install 第$try_count 次失败,准备重试..." +done + +if [ $success -eq 0 ]; then + echo "[ERROR] 阿里云镜像 bundle install 失败$max_try次,切换为官方源重试..." + bundle config mirror.https://rubygems.org https://rubygems.org + bundle config set --local path "$BUNDLE_PATH" + bundle install --gemfile "$GEMFILE_PATH" + if [ $? -eq 0 ]; then + echo "[SUCCESS] 官方源 bundle install 成功" + exit 0 + else + echo "[FATAL] 官方源 bundle install 依然失败,请检查网络或Gemfile配置。" + exit 2 + fi +fi +exit 0 \ No newline at end of file From e035d5105edd395d8f99153339aaafa9d4d50d34 Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 16 May 2025 17:39:53 +0800 Subject: [PATCH 117/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4tag=E8=A7=A6?= =?UTF-8?q?=E5=8F=91=E6=9E=84=E5=BB=BA=E7=9A=84=E6=AD=A3=E5=88=99=E5=88=A4?= =?UTF-8?q?=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/build.sh | 15 ++++++++++----- ios/build.sh | 10 +++++++--- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/android/build.sh b/android/build.sh index f444cb1e..e002bba7 100755 --- a/android/build.sh +++ b/android/build.sh @@ -8,7 +8,7 @@ export ENV_BUILD_WORKSPACE=${CI_PROJECT_DIR} echo "GITLAB_WORKSPACE: ${CI_PROJECT_DIR}" cd ${CI_PROJECT_DIR}/android echo "ENV_BUILD_TAG:${ENV_BUILD_TAG},ENV_BUILD_BRANCH:${ENV_BUILD_BRANCH}" -regex='^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]+\.[0-9]+)?$' +regex='^(sky_)?v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]+\.[0-9]+)?$' if [[ "${ENV_BUILD_BRANCH}" == "canary_release" ]]; then echo "===build canary_release: ${NEXT_VERSION}" export ENV_BUILD_TAG=${NEXT_VERSION} @@ -16,10 +16,15 @@ if [[ "${ENV_BUILD_BRANCH}" == "canary_release" ]]; then bundle exec fastlane release_apk flavor:sky --verbose elif [[ $ENV_BUILD_TAG =~ $regex ]]; then echo "===build release===$ENV_BUILD_TAG" - bundle exec fastlane release_apk flavor:xhj --verbose - bundle exec fastlane release_apk flavor:sky --verbose - bundle exec fastlane release_bundle flavor:xhj_bundle --verbose - bundle exec fastlane release_bundle flavor:sky --verbose + if [[ "${ENV_BUILD_TAG}" =~ ^sky_ ]]; then + bundle exec fastlane release_apk flavor:sky --verbose + bundle exec fastlane release_bundle flavor:sky --verbose + else + bundle exec fastlane release_apk flavor:xhj --verbose + bundle exec fastlane release_apk flavor:sky --verbose + bundle exec fastlane release_bundle flavor:xhj_bundle --verbose + bundle exec fastlane release_bundle flavor:sky --verbose + fi elif [[ "${ENV_BUILD_BRANCH}" == "develop" ]]; then echo "===build dev===${NEXT_VERSION}" bundle exec fastlane beta flavor:xhj env:dev --verbose diff --git a/ios/build.sh b/ios/build.sh index b492d623..dbcc67d2 100755 --- a/ios/build.sh +++ b/ios/build.sh @@ -9,7 +9,7 @@ echo "GITLAB_WORKSPACE: ${CI_PROJECT_DIR}" cd ${CI_PROJECT_DIR}/ios #bundle exec pod install echo "ENV_BUILD_TAG:${ENV_BUILD_TAG},ENV_BUILD_BRANCH:${ENV_BUILD_BRANCH}" -regex='^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]+\.[0-9]+)?$' +regex='^(sky_)?v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]+\.[0-9]+)?$' # ==== 调试输出,确认环境和依赖 ==== echo "=== FASTLANE/GEM/ENV DEBUG ===" @@ -29,8 +29,12 @@ if [[ "${ENV_BUILD_BRANCH}" == "canary_release" ]]; then bundle exec fastlane release_ipa flavor:sky --verbose elif [[ $ENV_BUILD_TAG =~ $regex ]]; then echo "===build release===$ENV_BUILD_TAG" - bundle exec fastlane release_ipa flavor:xhj --verbose - bundle exec fastlane release_ipa flavor:sky --verbose + if [[ "${ENV_BUILD_TAG}" =~ ^sky_ ]]; then + bundle exec fastlane release_ipa flavor:sky --verbose + else + bundle exec fastlane release_ipa flavor:xhj --verbose + bundle exec fastlane release_ipa flavor:sky --verbose + fi elif [[ "${ENV_BUILD_BRANCH}" == "develop" ]]; then echo "===build dev===${NEXT_VERSION}" bundle exec fastlane beta flavor:xhj env:Dev --verbose From a44be677c5fa14e7dd028c522490122b432b756e Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 16 May 2025 17:52:48 +0800 Subject: [PATCH 118/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4source=E6=BA=90?= =?UTF-8?q?=E8=AE=A9bundle=5Finstall=5Fretry=E8=84=9A=E6=9C=AC=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/Gemfile | 2 +- ios/Gemfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/android/Gemfile b/android/Gemfile index 79087196..5f5b8322 100644 --- a/android/Gemfile +++ b/android/Gemfile @@ -1,4 +1,4 @@ -source "https://mirrors.aliyun.com/rubygems/" +source "https://rubygems.org" gem "fastlane" gem 'nkf', '0.2.0' diff --git a/ios/Gemfile b/ios/Gemfile index 8a7082c8..cb7538c4 100644 --- a/ios/Gemfile +++ b/ios/Gemfile @@ -1,4 +1,4 @@ -source "https://gems.ruby-china.com" +source "https://rubygems.org" gem "fastlane" gem 'cocoapods', '1.14.3' From bed58065b0b060d5b38f4eef88b667a30bb921cf Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 16 May 2025 18:01:08 +0800 Subject: [PATCH 119/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4tag=E6=98=B5?= =?UTF-8?q?=E7=A7=B0=E5=88=A4=E6=96=AD=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/fastlane/Fastfile | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 3591770d..702fd5aa 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -132,7 +132,12 @@ platform :ios do print_log "build flavor: #{flavor}" build_number = Time.now.strftime("%Y%m%d%H%M") print_log "Build Commits #{build_number}" - build_version = $current_tag.match(/^v(\d+\.\d+\.\d+)/).captures[0] + m = $current_tag.match(/^(sky_)?v(\d+\.\d+\.\d+)/) + if m + build_version = m.captures.last + else + UI.user_error!("Tag格式不正确,无法提取版本号: #{$current_tag}") + end print_log "build_version #{build_version}" commit_hash = last_git_commit short_hash = commit_hash[:abbreviated_commit_hash] From 5ddfbe46d1208efe98f59ca19d0d834d024cb48c Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 16 May 2025 18:18:10 +0800 Subject: [PATCH 120/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E5=87=BA?= =?UTF-8?q?=E7=8E=B0Could=20not=20find=E7=9A=84=E9=97=AE=E9=A2=98=EF=BC=8C?= =?UTF-8?q?=E6=98=BE=E5=BC=8F=E6=8C=87=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 4 +- scripts/bundle_install_and_auto_add.sh | 79 ++++++++++++++++++++++++++ scripts/bundle_missing_gem_auto_add.sh | 49 ++++++++++++++++ 3 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 scripts/bundle_install_and_auto_add.sh create mode 100644 scripts/bundle_missing_gem_auto_add.sh diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1e07d557..cfe01e10 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -76,7 +76,7 @@ variables: - bundle -v || gem install bundler --source https://gems.ruby-china.com/ - ls -li - export NEXT_VERSION="$(cat app_new.version)" - - bash scripts/bundle_install_retry.sh android/Gemfile vendor/bundle_android + - bash scripts/bundle_install_and_auto_add.sh android/Gemfile vendor/bundle_android "bundle exec fastlane -v" - gem pristine --all || true # 修复所有未编译的gem扩展 script: # 输出调试信息,便于后续排查环境问题 @@ -107,7 +107,7 @@ variables: - bundle -v || gem install bundler --source https://gems.ruby-china.com/ - ls -li - export NEXT_VERSION="$(cat app_new.version)" - - bash scripts/bundle_install_retry.sh ios/Gemfile vendor/bundle_ios + - bash scripts/bundle_install_and_auto_add.sh ios/Gemfile vendor/bundle_ios "bundle exec fastlane -v" - gem pristine --all || true # 修复所有未编译的gem扩展 script: # 输出调试信息,便于后续排查环境问题 diff --git a/scripts/bundle_install_and_auto_add.sh b/scripts/bundle_install_and_auto_add.sh new file mode 100644 index 00000000..5a1a8449 --- /dev/null +++ b/scripts/bundle_install_and_auto_add.sh @@ -0,0 +1,79 @@ +#!/bin/bash +# 用法: bash scripts/bundle_install_and_auto_add.sh +# 例如: bash scripts/bundle_install_and_auto_add.sh ios/Gemfile vendor/bundle_ios "bundle exec fastlane -v" + +GEMFILE_PATH="$1" +BUNDLE_PATH="$2" +MAIN_CMD="$3" + +if [ -z "$GEMFILE_PATH" ] || [ -z "$BUNDLE_PATH" ] || [ -z "$MAIN_CMD" ]; then + echo "用法: $0 " + exit 1 +fi + +max_auto_add=3 +add_count=0 +success=0 + +# 1. 检查并自动补全Gemfile缺失依赖 +while [ $add_count -lt $max_auto_add ]; do + echo "[INFO] 第$((add_count+1))次尝试运行主命令: $MAIN_CMD (仅捕获缺失gem)" + $MAIN_CMD > bundle_missing_gem.log 2>&1 && success=1 && break + missing_gem=$(grep -o "Could not find [^ ]*" bundle_missing_gem.log | awk '{print $4}' | head -n1) + if [ -n "$missing_gem" ]; then + if ! grep -q "gem '$missing_gem'" "$GEMFILE_PATH"; then + echo "gem '$missing_gem'" >> "$GEMFILE_PATH" + echo "[AUTO] Gemfile已自动补全: $missing_gem" + else + echo "[WARN] Gemfile已包含 $missing_gem,但依然缺失,可能是平台或缓存问题" + fi + else + echo "[INFO] 未检测到缺失gem,或主命令已成功。" + break + fi + add_count=$((add_count+1)) + sleep 2 + echo "[WARN] 第$add_count 次自动补全后重试..." +done + +if [ $success -eq 1 ]; then + echo "[SUCCESS] 所有依赖已补全,主命令执行成功。" + exit 0 +fi + +# 2. 统一执行镜像切换和重试的bundle install +try_count=0 +max_try=3 +success=0 +bundle config mirror.https://rubygems.org https://mirrors.aliyun.com/rubygems/ +while [ $try_count -lt $max_try ]; do + echo "[INFO] 第$((try_count+1))次尝试使用阿里云镜像 bundle install..." + bundle config set --local path "$BUNDLE_PATH" + bundle install --gemfile "$GEMFILE_PATH" + if [ $? -eq 0 ]; then + echo "[SUCCESS] 使用阿里云镜像 bundle install 成功" + success=1 + break + fi + try_count=$((try_count+1)) + sleep 2 + echo "[WARN] 阿里云镜像 bundle install 第$try_count 次失败,准备重试..." +done + +if [ $success -eq 0 ]; then + echo "[ERROR] 阿里云镜像 bundle install 失败$max_try次,切换为官方源重试..." + bundle config mirror.https://rubygems.org https://rubygems.org + bundle config set --local path "$BUNDLE_PATH" + bundle install --gemfile "$GEMFILE_PATH" + if [ $? -eq 0 ]; then + echo "[SUCCESS] 官方源 bundle install 成功" + else + echo "[FATAL] 官方源 bundle install 依然失败,请检查网络或Gemfile配置。" + exit 2 + fi +fi + +# 3. 最后再执行一次主命令校验 +$MAIN_CMD || { echo "[FATAL] 主命令依然失败,请人工检查Gemfile和依赖环境。"; exit 3; } +echo "[SUCCESS] 所有依赖已补全,主命令执行成功。" +exit 0 \ No newline at end of file diff --git a/scripts/bundle_missing_gem_auto_add.sh b/scripts/bundle_missing_gem_auto_add.sh new file mode 100644 index 00000000..fdc5cc64 --- /dev/null +++ b/scripts/bundle_missing_gem_auto_add.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# 用法: bash scripts/bundle_missing_gem_auto_add.sh +# 例如: bash scripts/bundle_missing_gem_auto_add.sh ios/Gemfile vendor/bundle_ios "bundle exec fastlane -v" + +GEMFILE_PATH="$1" +BUNDLE_PATH="$2" +MAIN_CMD="$3" + +if [ -z "$GEMFILE_PATH" ] || [ -z "$BUNDLE_PATH" ] || [ -z "$MAIN_CMD" ]; then + echo "用法: $0 " + exit 1 +fi + +max_try=3 +try_count=0 +success=0 + +bundle check --path "$BUNDLE_PATH" || bundle install --gemfile "$GEMFILE_PATH" --path "$BUNDLE_PATH" + +while [ $try_count -lt $max_try ]; do + echo "[INFO] 第$((try_count+1))次尝试运行主命令: $MAIN_CMD" + $MAIN_CMD > bundle_missing_gem.log 2>&1 && success=1 && break + # 捕获缺失gem + missing_gem=$(grep -o "Could not find [^ ]*" bundle_missing_gem.log | awk '{print $4}' | head -n1) + if [ -n "$missing_gem" ]; then + if ! grep -q "gem '$missing_gem'" "$GEMFILE_PATH"; then + echo "gem '$missing_gem'" >> "$GEMFILE_PATH" + echo "[AUTO] Gemfile已自动补全: $missing_gem" + else + echo "[WARN] Gemfile已包含 $missing_gem,但依然缺失,可能是平台或缓存问题" + fi + bundle install --gemfile "$GEMFILE_PATH" --path "$BUNDLE_PATH" + else + echo "[FATAL] 未检测到缺失gem,需人工介入。日志如下:" + cat bundle_missing_gem.log + exit 2 + fi + try_count=$((try_count+1)) + sleep 2 + echo "[WARN] 第$try_count 次自动补全后重试..." +done + +if [ $success -eq 1 ]; then + echo "[SUCCESS] 所有依赖已补全,主命令执行成功。" + exit 0 +else + echo "[FATAL] 自动补全$max_try次后依然失败,请人工检查Gemfile和依赖环境。" + exit 3 +fi \ No newline at end of file From 219e0e794f1e133943a2361279ffa0d5e90db6de Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 16 May 2025 18:24:58 +0800 Subject: [PATCH 121/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E5=87=BA?= =?UTF-8?q?=E7=8E=B0Could=20not=20find=E7=9A=84=E9=97=AE=E9=A2=98=EF=BC=8C?= =?UTF-8?q?=E6=98=BE=E5=BC=8F=E6=8C=87=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 8 +++-- scripts/bundle_install_retry.sh | 46 ------------------------ scripts/bundle_missing_gem_auto_add.sh | 49 -------------------------- 3 files changed, 6 insertions(+), 97 deletions(-) delete mode 100644 scripts/bundle_install_retry.sh delete mode 100644 scripts/bundle_missing_gem_auto_add.sh diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cfe01e10..32cf0991 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -64,7 +64,9 @@ variables: .setup_fastlane_android: extends: .build_rule before_script: - - rm -rf ~/.gem ~/.bundle vendor/bundle_android # 强烈建议每次清理,防止并发/缓存污染 + - rm -rf ~/.gem ~/.bundle vendor/bundle_android + - export GEM_HOME="$PWD/vendor/bundle_android" + - export GEM_PATH="$PWD/vendor/bundle_android" - export PATH="$HOME/.rbenv/bin:$PATH" - eval "$(rbenv init -)" - rbenv global 2.7.8 @@ -95,7 +97,9 @@ variables: .setup_fastlane_ios: extends: .build_rule before_script: - - rm -rf ~/.gem ~/.bundle vendor/bundle_ios # 强烈建议每次清理,防止并发/缓存污染 + - rm -rf ~/.gem ~/.bundle vendor/bundle_ios + - export GEM_HOME="$PWD/vendor/bundle_ios" + - export GEM_PATH="$PWD/vendor/bundle_ios" - export PATH="$HOME/.rbenv/bin:$PATH" - eval "$(rbenv init -)" - rbenv global 2.7.8 diff --git a/scripts/bundle_install_retry.sh b/scripts/bundle_install_retry.sh deleted file mode 100644 index 2678b221..00000000 --- a/scripts/bundle_install_retry.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/bash -# 用法: ./scripts/bundle_install_retry.sh -# 例如: ./scripts/bundle_install_retry.sh android/Gemfile vendor/bundle_android - -GEMFILE_PATH="$1" -BUNDLE_PATH="$2" - -if [ -z "$GEMFILE_PATH" ] || [ -z "$BUNDLE_PATH" ]; then - echo "用法: $0 " - exit 1 -fi - -try_count=0 -max_try=3 -success=0 - -# 先用阿里云镜像 -bundle config mirror.https://rubygems.org https://mirrors.aliyun.com/rubygems/ -while [ $try_count -lt $max_try ]; do - echo "[INFO] 第$((try_count+1))次尝试使用阿里云镜像 bundle install..." - bundle config set --local path "$BUNDLE_PATH" - bundle install --gemfile "$GEMFILE_PATH" - if [ $? -eq 0 ]; then - echo "[SUCCESS] 使用阿里云镜像 bundle install 成功" - success=1 - break - fi - try_count=$((try_count+1)) - sleep 2 - echo "[WARN] 阿里云镜像 bundle install 第$try_count 次失败,准备重试..." -done - -if [ $success -eq 0 ]; then - echo "[ERROR] 阿里云镜像 bundle install 失败$max_try次,切换为官方源重试..." - bundle config mirror.https://rubygems.org https://rubygems.org - bundle config set --local path "$BUNDLE_PATH" - bundle install --gemfile "$GEMFILE_PATH" - if [ $? -eq 0 ]; then - echo "[SUCCESS] 官方源 bundle install 成功" - exit 0 - else - echo "[FATAL] 官方源 bundle install 依然失败,请检查网络或Gemfile配置。" - exit 2 - fi -fi -exit 0 \ No newline at end of file diff --git a/scripts/bundle_missing_gem_auto_add.sh b/scripts/bundle_missing_gem_auto_add.sh deleted file mode 100644 index fdc5cc64..00000000 --- a/scripts/bundle_missing_gem_auto_add.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/bin/bash -# 用法: bash scripts/bundle_missing_gem_auto_add.sh -# 例如: bash scripts/bundle_missing_gem_auto_add.sh ios/Gemfile vendor/bundle_ios "bundle exec fastlane -v" - -GEMFILE_PATH="$1" -BUNDLE_PATH="$2" -MAIN_CMD="$3" - -if [ -z "$GEMFILE_PATH" ] || [ -z "$BUNDLE_PATH" ] || [ -z "$MAIN_CMD" ]; then - echo "用法: $0 " - exit 1 -fi - -max_try=3 -try_count=0 -success=0 - -bundle check --path "$BUNDLE_PATH" || bundle install --gemfile "$GEMFILE_PATH" --path "$BUNDLE_PATH" - -while [ $try_count -lt $max_try ]; do - echo "[INFO] 第$((try_count+1))次尝试运行主命令: $MAIN_CMD" - $MAIN_CMD > bundle_missing_gem.log 2>&1 && success=1 && break - # 捕获缺失gem - missing_gem=$(grep -o "Could not find [^ ]*" bundle_missing_gem.log | awk '{print $4}' | head -n1) - if [ -n "$missing_gem" ]; then - if ! grep -q "gem '$missing_gem'" "$GEMFILE_PATH"; then - echo "gem '$missing_gem'" >> "$GEMFILE_PATH" - echo "[AUTO] Gemfile已自动补全: $missing_gem" - else - echo "[WARN] Gemfile已包含 $missing_gem,但依然缺失,可能是平台或缓存问题" - fi - bundle install --gemfile "$GEMFILE_PATH" --path "$BUNDLE_PATH" - else - echo "[FATAL] 未检测到缺失gem,需人工介入。日志如下:" - cat bundle_missing_gem.log - exit 2 - fi - try_count=$((try_count+1)) - sleep 2 - echo "[WARN] 第$try_count 次自动补全后重试..." -done - -if [ $success -eq 1 ]; then - echo "[SUCCESS] 所有依赖已补全,主命令执行成功。" - exit 0 -else - echo "[FATAL] 自动补全$max_try次后依然失败,请人工检查Gemfile和依赖环境。" - exit 3 -fi \ No newline at end of file From c1c4a4a675407040ddd7874ac4dc5faead76b1d9 Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 16 May 2025 18:46:45 +0800 Subject: [PATCH 122/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E4=B8=8D?= =?UTF-8?q?=E6=A0=B9=E6=8D=AEtag=E6=98=B5=E7=A7=B0=E8=BF=9B=E8=A1=8C?= =?UTF-8?q?=E5=8C=BA=E5=88=86=E6=9E=84=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/build.sh | 14 +++++--------- ios/build.sh | 10 ++++------ 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/android/build.sh b/android/build.sh index e002bba7..04f83062 100755 --- a/android/build.sh +++ b/android/build.sh @@ -16,15 +16,11 @@ if [[ "${ENV_BUILD_BRANCH}" == "canary_release" ]]; then bundle exec fastlane release_apk flavor:sky --verbose elif [[ $ENV_BUILD_TAG =~ $regex ]]; then echo "===build release===$ENV_BUILD_TAG" - if [[ "${ENV_BUILD_TAG}" =~ ^sky_ ]]; then - bundle exec fastlane release_apk flavor:sky --verbose - bundle exec fastlane release_bundle flavor:sky --verbose - else - bundle exec fastlane release_apk flavor:xhj --verbose - bundle exec fastlane release_apk flavor:sky --verbose - bundle exec fastlane release_bundle flavor:xhj_bundle --verbose - bundle exec fastlane release_bundle flavor:sky --verbose - fi + bundle exec fastlane release_apk flavor:xhj --verbose + bundle exec fastlane release_apk flavor:sky --verbose + bundle exec fastlane release_bundle flavor:xhj_bundle --verbose + bundle exec fastlane release_bundle flavor:sky --verbose + ls -l build/app/outputs/flutter-apk/ elif [[ "${ENV_BUILD_BRANCH}" == "develop" ]]; then echo "===build dev===${NEXT_VERSION}" bundle exec fastlane beta flavor:xhj env:dev --verbose diff --git a/ios/build.sh b/ios/build.sh index dbcc67d2..af047e71 100755 --- a/ios/build.sh +++ b/ios/build.sh @@ -29,12 +29,10 @@ if [[ "${ENV_BUILD_BRANCH}" == "canary_release" ]]; then bundle exec fastlane release_ipa flavor:sky --verbose elif [[ $ENV_BUILD_TAG =~ $regex ]]; then echo "===build release===$ENV_BUILD_TAG" - if [[ "${ENV_BUILD_TAG}" =~ ^sky_ ]]; then - bundle exec fastlane release_ipa flavor:sky --verbose - else - bundle exec fastlane release_ipa flavor:xhj --verbose - bundle exec fastlane release_ipa flavor:sky --verbose - fi + # 无论tag前缀,均构建xhj和sky的ipa + bundle exec fastlane release_ipa flavor:xhj --verbose + bundle exec fastlane release_ipa flavor:sky --verbose + ls -l build/app/outputs/flutter-ipa/ elif [[ "${ENV_BUILD_BRANCH}" == "develop" ]]; then echo "===build dev===${NEXT_VERSION}" bundle exec fastlane beta flavor:xhj env:Dev --verbose From f42659c8ac08e5468be01ece226f6ddbb5186753 Mon Sep 17 00:00:00 2001 From: liyi Date: Sat, 17 May 2025 09:28:28 +0800 Subject: [PATCH 123/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 6 ++++-- android/Gemfile | 2 +- android/build.sh | 24 ++++++++++++------------ ios/Gemfile | 2 +- ios/build.sh | 16 ++++++++-------- scripts/bundle_install_and_auto_add.sh | 7 +++++++ 6 files changed, 33 insertions(+), 24 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 32cf0991..e0452619 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -73,7 +73,8 @@ variables: - export PATH="$HOME/.rbenv/shims:$PATH" - which ruby # 输出当前使用的ruby路径,便于调试 - ruby -v # 输出当前ruby版本,便于调试 - - gem sources --add https://gems.ruby-china.com/ --remove https://rubygems.org/ # 如在国外可移除此行 + - gem sources --add https://gems.ruby-china.com/ # 如在国外可移除此行 + - gem sources --add https://rubygems.org || true # 保证官方源始终存在 - bundle config mirror.https://rubygems.org https://mirrors.aliyun.com/rubygems/ - bundle -v || gem install bundler --source https://gems.ruby-china.com/ - ls -li @@ -106,7 +107,8 @@ variables: - export PATH="$HOME/.rbenv/shims:$PATH" - which ruby # 输出当前使用的ruby路径,便于调试 - ruby -v # 输出当前ruby版本,便于调试 - - gem sources --add https://gems.ruby-china.com/ --remove https://rubygems.org/ # 如在国外可移除此行 + - gem sources --add https://gems.ruby-china.com/ # 如在国外可移除此行 + - gem sources --add https://rubygems.org || true # 保证官方源始终存在 - bundle config mirror.https://rubygems.org https://gems.ruby-china.com - bundle -v || gem install bundler --source https://gems.ruby-china.com/ - ls -li diff --git a/android/Gemfile b/android/Gemfile index 5f5b8322..3de6080f 100644 --- a/android/Gemfile +++ b/android/Gemfile @@ -1,4 +1,4 @@ -source "https://rubygems.org" +source "https://gems.ruby-china.com" gem "fastlane" gem 'nkf', '0.2.0' diff --git a/android/build.sh b/android/build.sh index 04f83062..c6dd6d91 100755 --- a/android/build.sh +++ b/android/build.sh @@ -12,26 +12,26 @@ regex='^(sky_)?v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]+\.[0-9]+)?$' if [[ "${ENV_BUILD_BRANCH}" == "canary_release" ]]; then echo "===build canary_release: ${NEXT_VERSION}" export ENV_BUILD_TAG=${NEXT_VERSION} - bundle exec fastlane release_apk flavor:xhj --verbose - bundle exec fastlane release_apk flavor:sky --verbose + bundle exec fastlane release_apk flavor:xhj --verbose || { echo "[FATAL] fastlane release_apk xhj 失败"; exit 10; } + bundle exec fastlane release_apk flavor:sky --verbose || { echo "[FATAL] fastlane release_apk sky 失败"; exit 11; } elif [[ $ENV_BUILD_TAG =~ $regex ]]; then echo "===build release===$ENV_BUILD_TAG" - bundle exec fastlane release_apk flavor:xhj --verbose - bundle exec fastlane release_apk flavor:sky --verbose - bundle exec fastlane release_bundle flavor:xhj_bundle --verbose - bundle exec fastlane release_bundle flavor:sky --verbose + bundle exec fastlane release_apk flavor:xhj --verbose || { echo "[FATAL] fastlane release_apk xhj 失败"; exit 10; } + bundle exec fastlane release_apk flavor:sky --verbose || { echo "[FATAL] fastlane release_apk sky 失败"; exit 11; } + bundle exec fastlane release_bundle flavor:xhj_bundle --verbose || { echo "[FATAL] fastlane release_bundle xhj_bundle 失败"; exit 12; } + bundle exec fastlane release_bundle flavor:sky --verbose || { echo "[FATAL] fastlane release_bundle sky 失败"; exit 13; } ls -l build/app/outputs/flutter-apk/ elif [[ "${ENV_BUILD_BRANCH}" == "develop" ]]; then echo "===build dev===${NEXT_VERSION}" - bundle exec fastlane beta flavor:xhj env:dev --verbose - bundle exec fastlane beta flavor:sky env:dev --verbose + bundle exec fastlane beta flavor:xhj env:dev --verbose || { echo "[FATAL] fastlane beta xhj dev 失败"; exit 20; } + bundle exec fastlane beta flavor:sky env:dev --verbose || { echo "[FATAL] fastlane beta sky dev 失败"; exit 21; } elif [[ "${ENV_BUILD_BRANCH}" == "release" ]] || [[ "${ENV_BUILD_BRANCH}" == "feat_devops" ]] ; then echo "===build pre===${NEXT_VERSION}" - bundle exec fastlane beta flavor:xhj env:pre --verbose - bundle exec fastlane beta flavor:sky env:pre --verbose + bundle exec fastlane beta flavor:xhj env:pre --verbose || { echo "[FATAL] fastlane beta xhj pre 失败"; exit 30; } + bundle exec fastlane beta flavor:sky env:pre --verbose || { echo "[FATAL] fastlane beta sky pre 失败"; exit 31; } elif [[ "${ENV_BUILD_BRANCH}" == "release_sky" ]]; then echo "===build release_sky===${NEXT_VERSION}" - bundle exec fastlane release_apk flavor:sky --verbose - bundle exec fastlane release_bundle flavor:sky --verbose + bundle exec fastlane release_apk flavor:sky --verbose || { echo "[FATAL] fastlane release_apk sky 失败"; exit 40; } + bundle exec fastlane release_bundle flavor:sky --verbose || { echo "[FATAL] fastlane release_bundle sky 失败"; exit 41; } fi exit 0 \ No newline at end of file diff --git a/ios/Gemfile b/ios/Gemfile index cb7538c4..8a7082c8 100644 --- a/ios/Gemfile +++ b/ios/Gemfile @@ -1,4 +1,4 @@ -source "https://rubygems.org" +source "https://gems.ruby-china.com" gem "fastlane" gem 'cocoapods', '1.14.3' diff --git a/ios/build.sh b/ios/build.sh index af047e71..e7235771 100755 --- a/ios/build.sh +++ b/ios/build.sh @@ -25,21 +25,21 @@ gem list | grep digest-crc if [[ "${ENV_BUILD_BRANCH}" == "canary_release" ]]; then echo "===build canary_release: ${NEXT_VERSION}" export ENV_BUILD_TAG=${NEXT_VERSION} - bundle exec fastlane release_ipa flavor:xhj --verbose - bundle exec fastlane release_ipa flavor:sky --verbose + bundle exec fastlane release_ipa flavor:xhj --verbose || { echo "[FATAL] fastlane release_ipa xhj 失败"; exit 10; } + bundle exec fastlane release_ipa flavor:sky --verbose || { echo "[FATAL] fastlane release_ipa sky 失败"; exit 11; } elif [[ $ENV_BUILD_TAG =~ $regex ]]; then echo "===build release===$ENV_BUILD_TAG" # 无论tag前缀,均构建xhj和sky的ipa - bundle exec fastlane release_ipa flavor:xhj --verbose - bundle exec fastlane release_ipa flavor:sky --verbose + bundle exec fastlane release_ipa flavor:xhj --verbose || { echo "[FATAL] fastlane release_ipa xhj 失败"; exit 10; } + bundle exec fastlane release_ipa flavor:sky --verbose || { echo "[FATAL] fastlane release_ipa sky 失败"; exit 11; } ls -l build/app/outputs/flutter-ipa/ elif [[ "${ENV_BUILD_BRANCH}" == "develop" ]]; then echo "===build dev===${NEXT_VERSION}" - bundle exec fastlane beta flavor:xhj env:Dev --verbose - bundle exec fastlane beta flavor:sky env:Dev --verbose + bundle exec fastlane beta flavor:xhj env:Dev --verbose || { echo "[FATAL] fastlane beta xhj Dev 失败"; exit 20; } + bundle exec fastlane beta flavor:sky env:Dev --verbose || { echo "[FATAL] fastlane beta sky Dev 失败"; exit 21; } elif [[ "${ENV_BUILD_BRANCH}" == "release" ]] || [[ "${ENV_BUILD_BRANCH}" == "feat_devops" ]] ; then echo "===build pre===${NEXT_VERSION}" - bundle exec fastlane beta flavor:xhj env:Pre --verbose - bundle exec fastlane beta flavor:sky env:Pre --verbose + bundle exec fastlane beta flavor:xhj env:Pre --verbose || { echo "[FATAL] fastlane beta xhj Pre 失败"; exit 30; } + bundle exec fastlane beta flavor:sky env:Pre --verbose || { echo "[FATAL] fastlane beta sky Pre 失败"; exit 31; } fi exit 0 \ No newline at end of file diff --git a/scripts/bundle_install_and_auto_add.sh b/scripts/bundle_install_and_auto_add.sh index 5a1a8449..61fce330 100644 --- a/scripts/bundle_install_and_auto_add.sh +++ b/scripts/bundle_install_and_auto_add.sh @@ -11,6 +11,9 @@ if [ -z "$GEMFILE_PATH" ] || [ -z "$BUNDLE_PATH" ] || [ -z "$MAIN_CMD" ]; then exit 1 fi +# 保证rubygems.org官方源始终存在(开头) +gem sources --add https://rubygems.org || true + max_auto_add=3 add_count=0 success=0 @@ -38,6 +41,8 @@ done if [ $success -eq 1 ]; then echo "[SUCCESS] 所有依赖已补全,主命令执行成功。" + # 再次保证rubygems.org官方源始终存在(结尾) + gem sources --add https://rubygems.org || true exit 0 fi @@ -75,5 +80,7 @@ fi # 3. 最后再执行一次主命令校验 $MAIN_CMD || { echo "[FATAL] 主命令依然失败,请人工检查Gemfile和依赖环境。"; exit 3; } +# 再次保证rubygems.org官方源始终存在(结尾) +gem sources --add https://rubygems.org || true echo "[SUCCESS] 所有依赖已补全,主命令执行成功。" exit 0 \ No newline at end of file From 46bec40eb700cfce4f27141875e50c44adea01f5 Mon Sep 17 00:00:00 2001 From: liyi Date: Sat, 17 May 2025 10:17:43 +0800 Subject: [PATCH 124/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/fastlane/Fastfile | 93 +++++++++++++++++++++++++++++---------- ios/fastlane/Fastfile | 62 +++++++++++++++++--------- 2 files changed, 111 insertions(+), 44 deletions(-) diff --git a/android/fastlane/Fastfile b/android/fastlane/Fastfile index 2c66fff4..c8fe4f09 100644 --- a/android/fastlane/Fastfile +++ b/android/fastlane/Fastfile @@ -44,22 +44,29 @@ platform :android do print_header '🏁 Before All' print_log $current_branch print_log $current_tag - print_log $path_file_preview_apk_default - print_log $path_file_release_apk_default - print_log $path_file_preview_apk_copy - print_log $path_file_release_apk_copy Dir.chdir "../.." do sh('pwd') end + # RubyGems/bundle环境相关日志 + sh('which ruby') + sh('ruby -v') + sh('which gem') + sh('gem -v') + sh('gem sources -l') + sh('which bundle') + sh('bundle -v') + sh('bundle config') + sh('env | grep GEM || true') + sh('env | grep BUNDLE || true') + sh('env | grep CI_ || true') end desc "Submit a new Beta Build to Pgy Beta" lane :beta do |options| - flavor = options[:flavor] env = options[:env] - UI.user_error!("flavor is required") unless flavor + flavor = 'sky' UI.user_error!("env is required") unless env - print_log "build #{flavor} on #{env}" + print_log "build sky on #{env}" build_number = Time.now.strftime("%Y%m%d%H") print_log "BuildNo #{build_number}" build_version = $next_version @@ -69,11 +76,25 @@ platform :android do print_log "last_git_commit_short_hash #{short_hash}" remove_zone_pre_build(zone:"com") Dir.chdir "../.." do - sh("flutter","pub","get") - sh("flutter", "build", "apk", "--no-tree-shake-icons", "--release", "--flavor", "#{flavor}_#{env}", "-t", "lib/main_#{flavor}_#{env}.dart", "--build-number=#{build_number}", "--build-name=#{build_version}") + sh('pwd') + sh('ls -lh') + sh('ls -lh build/app/outputs/flutter-apk/ || true') + sh('ls -lh build/app/outputs/bundle/ || true') + sh("flutter","pub","get") end - old_file_path = File.join($path_apk_output_dir, "app-#{flavor}_#{env}-release.apk") - new_file_path = File.join($path_apk_output_dir, "starlock-#{flavor}-preview-#{build_version}.apk") + Dir.chdir "../.." do + sh('pwd') + sh('ls -lh') + sh('ls -lh build/app/outputs/flutter-apk/ || true') + sh('ls -lh build/app/outputs/bundle/ || true') + sh("flutter", "build", "apk", "--no-tree-shake-icons", "--release", "--flavor", "sky_#{env}", "-t", "lib/main_sky_#{env}.dart", "--build-number=#{build_number}", "--build-name=#{build_version}") + sh('ls -lh build/app/outputs/flutter-apk/ || true') + sh('ls -lh build/app/outputs/bundle/ || true') + end + old_file_path = File.join($path_apk_output_dir, "app-sky_#{env}-release.apk") + new_file_path = File.join($path_apk_output_dir, "starlock-sky-preview-#{build_version}.apk") + print_log "old_file_path: #{old_file_path}" + print_log "new_file_path: #{new_file_path}" File.rename(old_file_path, new_file_path) logs = changelog_from_git_commits( pretty: '- %s (%cn)', @@ -85,9 +106,8 @@ platform :android do desc "Build & upload a new version to Gitlab Release" lane :release_apk do |options| - flavor = options[:flavor] - UI.user_error!("flavor is required") unless flavor - print_log "build flavor for: #{flavor}" + flavor = 'sky' + print_log "build flavor for: sky" build_number = Time.now.strftime("%Y%m%d%H") print_log "BuildNo #{build_number}" build_version = $current_tag.match(/^(sky_)?v(\d+\.\d+\.\d+)/).captures.last @@ -97,19 +117,32 @@ platform :android do print_log "last_git_commit_short_hash #{short_hash}" remove_zone_pre_build(zone:"com") Dir.chdir "../.." do + sh('pwd') + sh('ls -lh') + sh('ls -lh build/app/outputs/flutter-apk/ || true') + sh('ls -lh build/app/outputs/bundle/ || true') sh("flutter","pub","get") - sh("flutter", "build", "apk", "--no-tree-shake-icons", "--release", "--flavor", "#{flavor}", "-t", "lib/main_#{flavor}_lite.dart", "--build-number=#{build_number}", "--build-name=#{build_version}") end - old_apk_file_path = File.join($path_apk_output_dir, "app-#{flavor}-release.apk") - new_apk_file_path = File.join($path_apk_output_dir, "starlock-#{flavor}-release-"+$current_tag+".apk") + Dir.chdir "../.." do + sh('pwd') + sh('ls -lh') + sh('ls -lh build/app/outputs/flutter-apk/ || true') + sh('ls -lh build/app/outputs/bundle/ || true') + sh("flutter", "build", "apk", "--no-tree-shake-icons", "--release", "--flavor", "sky", "-t", "lib/main_sky_lite.dart", "--build-number=#{build_number}", "--build-name=#{build_version}") + sh('ls -lh build/app/outputs/flutter-apk/ || true') + sh('ls -lh build/app/outputs/bundle/ || true') + end + old_apk_file_path = File.join($path_apk_output_dir, "app-sky-release.apk") + new_apk_file_path = File.join($path_apk_output_dir, "starlock-sky-release-"+$current_tag+".apk") + print_log "old_apk_file_path: #{old_apk_file_path}" + print_log "new_apk_file_path: #{new_apk_file_path}" File.rename(old_apk_file_path, new_apk_file_path) end desc "Build & upload a new version to Gitlab Release" lane :release_bundle do |options| - flavor = options[:flavor] - UI.user_error!("flavor is required") unless flavor - print_log "build flavor for: #{flavor}" + flavor = 'sky' + print_log "build flavor for: sky" build_number = Time.now.strftime("%Y%m%d%H") print_log "BuildNo #{build_number}" build_version = $current_tag.match(/^(sky_)?v(\d+\.\d+\.\d+)/).captures.last @@ -119,11 +152,25 @@ platform :android do print_log "last_git_commit_short_hash #{short_hash}" remove_zone_pre_build(zone:"cn") Dir.chdir "../.." do + sh('pwd') + sh('ls -lh') + sh('ls -lh build/app/outputs/flutter-apk/ || true') + sh('ls -lh build/app/outputs/bundle/ || true') sh("flutter","pub","get") - sh("flutter", "build", "appbundle", "--no-tree-shake-icons", "--release", "--flavor", "#{flavor}", "-t", "lib/main_#{flavor}_lite.dart", "--build-number=#{build_number}", "--build-name=#{build_version}") end - old_bundle_file_path = File.join($path_bundle_output_dir , "/#{flavor}Release/app-#{flavor}-release.aab") - new_bundle_file_path = File.join($path_bundle_output_dir , "/#{flavor}Release/starlock-#{flavor}-release-"+$current_tag+".aab") + Dir.chdir "../.." do + sh('pwd') + sh('ls -lh') + sh('ls -lh build/app/outputs/flutter-apk/ || true') + sh('ls -lh build/app/outputs/bundle/ || true') + sh("flutter", "build", "appbundle", "--no-tree-shake-icons", "--release", "--flavor", "sky", "-t", "lib/main_sky_lite.dart", "--build-number=#{build_number}", "--build-name=#{build_version}") + sh('ls -lh build/app/outputs/flutter-apk/ || true') + sh('ls -lh build/app/outputs/bundle/ || true') + end + old_bundle_file_path = File.join($path_bundle_output_dir , "/skyRelease/app-sky-release.aab") + new_bundle_file_path = File.join($path_bundle_output_dir , "/skyRelease/starlock-sky-release-"+$current_tag+".aab") + print_log "old_bundle_file_path: #{old_bundle_file_path}" + print_log "new_bundle_file_path: #{new_bundle_file_path}" File.rename(old_bundle_file_path, new_bundle_file_path) sh('cp',new_bundle_file_path,$path_apk_output_dir) end diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 702fd5aa..dc7bc668 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -68,38 +68,55 @@ platform :ios do Dir.chdir "../.." do sh('pwd') end + # RubyGems/bundle环境相关日志 + sh('which ruby') + sh('ruby -v') + sh('which gem') + sh('gem -v') + sh('gem sources -l') + sh('which bundle') + sh('bundle -v') + sh('bundle config') + sh('env | grep GEM || true') + sh('env | grep BUNDLE || true') + sh('env | grep CI_ || true') end desc "Build & Deliver to Pgy" lane :beta do |options| - flavor = options[:flavor] env = options[:env] - UI.user_error!("flavor is required") unless flavor + flavor = 'sky' UI.user_error!("env is required") unless env - print_log "build #{flavor} on #{env}" + print_log "build sky on #{env}" build_number = Time.now.strftime("%Y%m%d%H%M") print_log "Build Commits #{build_number}" - build_version = $next_version # Time.now.strftime("%Y%m%d%H%M%S") + build_version = $next_version print_log "build_version #{build_version}" commit_hash = last_git_commit short_hash = commit_hash[:abbreviated_commit_hash] print_log "last_git_commit_short_hash #{short_hash}" remove_zone_pre_build(zone:"com") Dir.chdir "../.." do + sh('pwd') + sh('ls -lh') + sh('ls -lh build/app/outputs/flutter-ipa/ || true') sh("flutter","pub","get") end Dir.chdir ".." do sh("bundle", "exec" ,"pod", "install") end Dir.chdir "../.." do - sh("flutter", "build", "ios", "--no-tree-shake-icons", "--no-codesign", "--release", "--flavor", "#{flavor}", "-t", "lib/main_#{flavor}_#{env}.dart", "--build-number=#{build_number}", "--build-name=#{build_version}") + sh('pwd') + sh('ls -lh') + sh('ls -lh build/app/outputs/flutter-ipa/ || true') + sh("flutter", "build", "ios", "--no-tree-shake-icons", "--no-codesign", "--release", "--flavor", "sky", "-t", "lib/main_sky_#{env}.dart", "--build-number=#{build_number}", "--build-name=#{build_version}") + sh('ls -lh build/app/outputs/flutter-ipa/ || true') end - #set_bundle_short_version('./Runner/Info.plist', $current_branch) - gym_scheme_preview="#{flavor}" - print_log "gym_scheme_preview #{gym_scheme_preview}" - gym_configuration = "#{env}-release-#{flavor}" + gym_scheme_preview="sky" + print_log "gym_scheme_preview sky" + gym_configuration = "#{env}-release-sky" print_log "gym_configuration #{gym_configuration}" - ipa_default_filename = "starlock-#{flavor}-preview-#{build_version}.ipa" + ipa_default_filename = "starlock-sky-preview-#{build_version}.ipa" print_log "ipa_default_filename #{ipa_default_filename}" gym( scheme: gym_scheme_preview, @@ -110,8 +127,6 @@ platform :ios do export_method: "ad-hoc", export_options: { provisioningProfiles: { - "com.xhjcn.lock.dev" => "Adhoc_com.xhjcn.lock.dev.mobileprovision", - "com.xhjcn.lock.pre" => "Adhoc_com.xhjcn.lock.pre.mobileprovision", "com.skychip.lock.dev" => "Adhoc_com.skychip.lock.dev.mobileprovision", "com.skychip.lock.pre" => "Adhoc_com.skychip.lock.pre.mobileprovision", } @@ -127,9 +142,8 @@ platform :ios do desc "Build & Deliver to App Store Connect" lane :release_ipa do |options| - flavor = options[:flavor] - UI.user_error!("flavor is required") unless flavor - print_log "build flavor: #{flavor}" + flavor = 'sky' + print_log "build flavor: sky" build_number = Time.now.strftime("%Y%m%d%H%M") print_log "Build Commits #{build_number}" m = $current_tag.match(/^(sky_)?v(\d+\.\d+\.\d+)/) @@ -144,19 +158,26 @@ platform :ios do print_log "last_git_commit_short_hash #{short_hash}" remove_zone_pre_build(zone:"com") Dir.chdir "../.." do + sh('pwd') + sh('ls -lh') + sh('ls -lh build/app/outputs/flutter-ipa/ || true') sh("flutter","pub","get") end Dir.chdir ".." do sh("bundle", "exec" ,"pod", "install") end Dir.chdir "../.." do - sh("flutter", "build", "ios", "--no-tree-shake-icons", "--no-codesign", "--release", "--flavor", "#{flavor}", "-t", "lib/main_#{flavor}_lite.dart", "--build-number=#{build_number}", "--build-name=#{build_version}") + sh('pwd') + sh('ls -lh') + sh('ls -lh build/app/outputs/flutter-ipa/ || true') + sh("flutter", "build", "ios", "--no-tree-shake-icons", "--no-codesign", "--release", "--flavor", "sky", "-t", "lib/main_sky_lite.dart", "--build-number=#{build_number}", "--build-name=#{build_version}") + sh('ls -lh build/app/outputs/flutter-ipa/ || true') end - gym_scheme_release="#{flavor}" - print_log "gym_scheme_release #{gym_scheme_release}" - gym_configuration = "Release-#{flavor}" + gym_scheme_release="sky" + print_log "gym_scheme_release sky" + gym_configuration = "Release-sky" print_log "gym_configuration #{gym_configuration}" - ipa_default_filename = "starlock-#{flavor}-release-"+$current_tag+".ipa" + ipa_default_filename = "starlock-sky-release-"+$current_tag+".ipa" print_log "ipa_default_filename #{ipa_default_filename}" gym( scheme: gym_scheme_release, @@ -167,7 +188,6 @@ platform :ios do export_method: "app-store", export_options: { provisioningProfiles: { - "com.xhjcn.lock" => "Appstore_com.xhjcn.lock.mobileprovision", "com.skychip.lock" => "Appstore_com.skychip.lock.mobileprovision", } } From 804cdba152d9c77d480d000a2998c908a8136ca8 Mon Sep 17 00:00:00 2001 From: liyi Date: Sat, 17 May 2025 10:42:19 +0800 Subject: [PATCH 125/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 2 ++ scripts/bundle_install_and_auto_add.sh | 39 ++++++-------------------- 2 files changed, 11 insertions(+), 30 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e0452619..f6716abe 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -81,6 +81,7 @@ variables: - export NEXT_VERSION="$(cat app_new.version)" - bash scripts/bundle_install_and_auto_add.sh android/Gemfile vendor/bundle_android "bundle exec fastlane -v" - gem pristine --all || true # 修复所有未编译的gem扩展 + - echo -e "---\n:backtrace: false\n:bulk_threshold: 1000\n:sources:\n- https://rubygems.org\n:update_sources: true\n:verbose: true" > ~/.gemrc script: # 输出调试信息,便于后续排查环境问题 - echo "=== DEBUG INFO (android) ===" @@ -115,6 +116,7 @@ variables: - export NEXT_VERSION="$(cat app_new.version)" - bash scripts/bundle_install_and_auto_add.sh ios/Gemfile vendor/bundle_ios "bundle exec fastlane -v" - gem pristine --all || true # 修复所有未编译的gem扩展 + - echo -e "---\n:backtrace: false\n:bulk_threshold: 1000\n:sources:\n- https://rubygems.org\n:update_sources: true\n:verbose: true" > ~/.gemrc script: # 输出调试信息,便于后续排查环境问题 - echo "=== DEBUG INFO (ios) ===" diff --git a/scripts/bundle_install_and_auto_add.sh b/scripts/bundle_install_and_auto_add.sh index 61fce330..84fd6d2d 100644 --- a/scripts/bundle_install_and_auto_add.sh +++ b/scripts/bundle_install_and_auto_add.sh @@ -46,36 +46,15 @@ if [ $success -eq 1 ]; then exit 0 fi -# 2. 统一执行镜像切换和重试的bundle install -try_count=0 -max_try=3 -success=0 -bundle config mirror.https://rubygems.org https://mirrors.aliyun.com/rubygems/ -while [ $try_count -lt $max_try ]; do - echo "[INFO] 第$((try_count+1))次尝试使用阿里云镜像 bundle install..." - bundle config set --local path "$BUNDLE_PATH" - bundle install --gemfile "$GEMFILE_PATH" - if [ $? -eq 0 ]; then - echo "[SUCCESS] 使用阿里云镜像 bundle install 成功" - success=1 - break - fi - try_count=$((try_count+1)) - sleep 2 - echo "[WARN] 阿里云镜像 bundle install 第$try_count 次失败,准备重试..." -done - -if [ $success -eq 0 ]; then - echo "[ERROR] 阿里云镜像 bundle install 失败$max_try次,切换为官方源重试..." - bundle config mirror.https://rubygems.org https://rubygems.org - bundle config set --local path "$BUNDLE_PATH" - bundle install --gemfile "$GEMFILE_PATH" - if [ $? -eq 0 ]; then - echo "[SUCCESS] 官方源 bundle install 成功" - else - echo "[FATAL] 官方源 bundle install 依然失败,请检查网络或Gemfile配置。" - exit 2 - fi +# 2. 只用 rubygems.org 官方源进行 bundle install +bundle config mirror.https://rubygems.org https://rubygems.org +bundle config set --local path "$BUNDLE_PATH" +bundle install --gemfile "$GEMFILE_PATH" +if [ $? -eq 0 ]; then + echo "[SUCCESS] 官方源 bundle install 成功" +else + echo "[FATAL] 官方源 bundle install 失败,请检查网络或Gemfile配置。" + exit 2 fi # 3. 最后再执行一次主命令校验 From 1ee2ca5a54cf6b4fbc282c182e8c39f338493f4f Mon Sep 17 00:00:00 2001 From: liyi Date: Sat, 17 May 2025 11:22:56 +0800 Subject: [PATCH 126/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f6716abe..147f7607 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -75,13 +75,13 @@ variables: - ruby -v # 输出当前ruby版本,便于调试 - gem sources --add https://gems.ruby-china.com/ # 如在国外可移除此行 - gem sources --add https://rubygems.org || true # 保证官方源始终存在 + - 'echo -e "---\n:backtrace: false\n:bulk_threshold: 1000\n:sources:\n- https://rubygems.org\n:update_sources: true\n:verbose: true" > ~/.gemrc' - bundle config mirror.https://rubygems.org https://mirrors.aliyun.com/rubygems/ - bundle -v || gem install bundler --source https://gems.ruby-china.com/ - ls -li - export NEXT_VERSION="$(cat app_new.version)" - bash scripts/bundle_install_and_auto_add.sh android/Gemfile vendor/bundle_android "bundle exec fastlane -v" - gem pristine --all || true # 修复所有未编译的gem扩展 - - echo -e "---\n:backtrace: false\n:bulk_threshold: 1000\n:sources:\n- https://rubygems.org\n:update_sources: true\n:verbose: true" > ~/.gemrc script: # 输出调试信息,便于后续排查环境问题 - echo "=== DEBUG INFO (android) ===" @@ -110,13 +110,13 @@ variables: - ruby -v # 输出当前ruby版本,便于调试 - gem sources --add https://gems.ruby-china.com/ # 如在国外可移除此行 - gem sources --add https://rubygems.org || true # 保证官方源始终存在 - - bundle config mirror.https://rubygems.org https://gems.ruby-china.com + - 'echo -e "---\n:backtrace: false\n:bulk_threshold: 1000\n:sources:\n- https://rubygems.org\n:update_sources: true\n:verbose: true" > ~/.gemrc' + - bundle config mirror.https://rubygems.org https://mirrors.aliyun.com/rubygems/ - bundle -v || gem install bundler --source https://gems.ruby-china.com/ - ls -li - export NEXT_VERSION="$(cat app_new.version)" - bash scripts/bundle_install_and_auto_add.sh ios/Gemfile vendor/bundle_ios "bundle exec fastlane -v" - gem pristine --all || true # 修复所有未编译的gem扩展 - - echo -e "---\n:backtrace: false\n:bulk_threshold: 1000\n:sources:\n- https://rubygems.org\n:update_sources: true\n:verbose: true" > ~/.gemrc script: # 输出调试信息,便于后续排查环境问题 - echo "=== DEBUG INFO (ios) ===" From aa6f01ad5ca88edccdff0a65b3ab145be28a7c61 Mon Sep 17 00:00:00 2001 From: liyi Date: Sat, 17 May 2025 11:42:23 +0800 Subject: [PATCH 127/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4=E9=95=9C?= =?UTF-8?q?=E5=83=8F=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 8 ++------ android/Gemfile | 2 +- ios/Gemfile | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 147f7607..69000a2c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -73,11 +73,9 @@ variables: - export PATH="$HOME/.rbenv/shims:$PATH" - which ruby # 输出当前使用的ruby路径,便于调试 - ruby -v # 输出当前ruby版本,便于调试 - - gem sources --add https://gems.ruby-china.com/ # 如在国外可移除此行 - gem sources --add https://rubygems.org || true # 保证官方源始终存在 - 'echo -e "---\n:backtrace: false\n:bulk_threshold: 1000\n:sources:\n- https://rubygems.org\n:update_sources: true\n:verbose: true" > ~/.gemrc' - - bundle config mirror.https://rubygems.org https://mirrors.aliyun.com/rubygems/ - - bundle -v || gem install bundler --source https://gems.ruby-china.com/ + - bundle -v || gem install bundler --source https://rubygems.org/ - ls -li - export NEXT_VERSION="$(cat app_new.version)" - bash scripts/bundle_install_and_auto_add.sh android/Gemfile vendor/bundle_android "bundle exec fastlane -v" @@ -108,11 +106,9 @@ variables: - export PATH="$HOME/.rbenv/shims:$PATH" - which ruby # 输出当前使用的ruby路径,便于调试 - ruby -v # 输出当前ruby版本,便于调试 - - gem sources --add https://gems.ruby-china.com/ # 如在国外可移除此行 - gem sources --add https://rubygems.org || true # 保证官方源始终存在 - 'echo -e "---\n:backtrace: false\n:bulk_threshold: 1000\n:sources:\n- https://rubygems.org\n:update_sources: true\n:verbose: true" > ~/.gemrc' - - bundle config mirror.https://rubygems.org https://mirrors.aliyun.com/rubygems/ - - bundle -v || gem install bundler --source https://gems.ruby-china.com/ + - bundle -v || gem install bundler --source https://rubygems.org/ - ls -li - export NEXT_VERSION="$(cat app_new.version)" - bash scripts/bundle_install_and_auto_add.sh ios/Gemfile vendor/bundle_ios "bundle exec fastlane -v" diff --git a/android/Gemfile b/android/Gemfile index 3de6080f..5f5b8322 100644 --- a/android/Gemfile +++ b/android/Gemfile @@ -1,4 +1,4 @@ -source "https://gems.ruby-china.com" +source "https://rubygems.org" gem "fastlane" gem 'nkf', '0.2.0' diff --git a/ios/Gemfile b/ios/Gemfile index 8a7082c8..cb7538c4 100644 --- a/ios/Gemfile +++ b/ios/Gemfile @@ -1,4 +1,4 @@ -source "https://gems.ruby-china.com" +source "https://rubygems.org" gem "fastlane" gem 'cocoapods', '1.14.3' From 7dde0cdf8ec7346a5ae34f27cc3d268a054097ea Mon Sep 17 00:00:00 2001 From: liyi Date: Sat, 17 May 2025 14:00:46 +0800 Subject: [PATCH 128/151] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0android=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/build.sh | 4 ++++ android/fastlane/Fastfile | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/android/build.sh b/android/build.sh index c6dd6d91..31d08255 100755 --- a/android/build.sh +++ b/android/build.sh @@ -20,6 +20,10 @@ elif [[ $ENV_BUILD_TAG =~ $regex ]]; then bundle exec fastlane release_apk flavor:sky --verbose || { echo "[FATAL] fastlane release_apk sky 失败"; exit 11; } bundle exec fastlane release_bundle flavor:xhj_bundle --verbose || { echo "[FATAL] fastlane release_bundle xhj_bundle 失败"; exit 12; } bundle exec fastlane release_bundle flavor:sky --verbose || { echo "[FATAL] fastlane release_bundle sky 失败"; exit 13; } + if [ ! -d build/app/outputs/flutter-apk/ ]; then + echo "❌ 产物目录 build/app/outputs/flutter-apk/ 未生成,构建失败!" + exit 99 + fi ls -l build/app/outputs/flutter-apk/ elif [[ "${ENV_BUILD_BRANCH}" == "develop" ]]; then echo "===build dev===${NEXT_VERSION}" diff --git a/android/fastlane/Fastfile b/android/fastlane/Fastfile index c8fe4f09..00e3b286 100644 --- a/android/fastlane/Fastfile +++ b/android/fastlane/Fastfile @@ -95,6 +95,9 @@ platform :android do new_file_path = File.join($path_apk_output_dir, "starlock-sky-preview-#{build_version}.apk") print_log "old_file_path: #{old_file_path}" print_log "new_file_path: #{new_file_path}" + unless File.exist?(old_file_path) + UI.user_error!("❌ 产物 #{old_file_path} 未生成,构建失败!") + end File.rename(old_file_path, new_file_path) logs = changelog_from_git_commits( pretty: '- %s (%cn)', @@ -136,6 +139,9 @@ platform :android do new_apk_file_path = File.join($path_apk_output_dir, "starlock-sky-release-"+$current_tag+".apk") print_log "old_apk_file_path: #{old_apk_file_path}" print_log "new_apk_file_path: #{new_apk_file_path}" + unless File.exist?(old_apk_file_path) + UI.user_error!("❌ 产物 #{old_apk_file_path} 未生成,构建失败!") + end File.rename(old_apk_file_path, new_apk_file_path) end @@ -171,7 +177,13 @@ platform :android do new_bundle_file_path = File.join($path_bundle_output_dir , "/skyRelease/starlock-sky-release-"+$current_tag+".aab") print_log "old_bundle_file_path: #{old_bundle_file_path}" print_log "new_bundle_file_path: #{new_bundle_file_path}" + unless File.exist?(old_bundle_file_path) + UI.user_error!("❌ 产物 #{old_bundle_file_path} 未生成,构建失败!") + end File.rename(old_bundle_file_path, new_bundle_file_path) + unless File.exist?(new_bundle_file_path) + UI.user_error!("❌ 产物 #{new_bundle_file_path} 未生成,构建失败!") + end sh('cp',new_bundle_file_path,$path_apk_output_dir) end From 03c24b3c977f5e46ff405390dba82ea13f65d9df Mon Sep 17 00:00:00 2001 From: liyi Date: Sat, 17 May 2025 14:44:46 +0800 Subject: [PATCH 129/151] =?UTF-8?q?fix:=E5=A2=9E=E5=8A=A0android=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E6=97=A5=E5=BF=97=EF=BC=8C=E9=81=BF=E5=85=8D=E5=B9=B6?= =?UTF-8?q?=E5=8F=91=E6=9E=84=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 4 ++++ android/build.sh | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 69000a2c..c2bffcb6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -177,6 +177,10 @@ build_android: build_ios: stage: build-artifacts extends: .setup_fastlane_ios + needs: + - build_android + dependencies: + - build_android script: - echo "=== DEBUG INFO (ios) ===" - which ruby diff --git a/android/build.sh b/android/build.sh index 31d08255..10618af0 100755 --- a/android/build.sh +++ b/android/build.sh @@ -20,10 +20,17 @@ elif [[ $ENV_BUILD_TAG =~ $regex ]]; then bundle exec fastlane release_apk flavor:sky --verbose || { echo "[FATAL] fastlane release_apk sky 失败"; exit 11; } bundle exec fastlane release_bundle flavor:xhj_bundle --verbose || { echo "[FATAL] fastlane release_bundle xhj_bundle 失败"; exit 12; } bundle exec fastlane release_bundle flavor:sky --verbose || { echo "[FATAL] fastlane release_bundle sky 失败"; exit 13; } + echo "=== fastlane lane 结束,检查产物目录(前) ===" + ls -lh build/app/outputs/flutter-apk/ || true + ls -lh build/app/outputs/bundle/ || true if [ ! -d build/app/outputs/flutter-apk/ ]; then echo "❌ 产物目录 build/app/outputs/flutter-apk/ 未生成,构建失败!" + echo "=== 产物目录消失时,build/app/outputs/ 内容如下 ===" + ls -lh build/app/outputs/ || true + ls -lh build/app/outputs/bundle/ || true exit 99 fi + echo "=== 产物目录检查通过,最终内容如下 ===" ls -l build/app/outputs/flutter-apk/ elif [[ "${ENV_BUILD_BRANCH}" == "develop" ]]; then echo "===build dev===${NEXT_VERSION}" From 9c4cea522466b5b57ba3193d80e86479285c71d9 Mon Sep 17 00:00:00 2001 From: liyi Date: Sat, 17 May 2025 15:12:13 +0800 Subject: [PATCH 130/151] =?UTF-8?q?fix:=E7=A7=BB=E9=99=A4=E9=AB=98?= =?UTF-8?q?=E5=BE=B7=E5=90=8C=E6=97=B6=E7=A7=BB=E9=99=A4=E9=94=81=E5=9F=BA?= =?UTF-8?q?=E6=9C=AC=E4=BF=A1=E6=81=AF=E4=B8=AD=E7=9A=84=E4=BD=8D=E7=BD=AE?= =?UTF-8?q?=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../basicInformation_page.dart | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/main/lockDetail/lockSet/basicInformation/basicInformation/basicInformation_page.dart b/lib/main/lockDetail/lockSet/basicInformation/basicInformation/basicInformation_page.dart index 58c5a9bb..20c36487 100755 --- a/lib/main/lockDetail/lockSet/basicInformation/basicInformation/basicInformation_page.dart +++ b/lib/main/lockDetail/lockSet/basicInformation/basicInformation/basicInformation_page.dart @@ -158,23 +158,23 @@ class _BasicInformationPageState extends State { allHeight: 70.h, isHaveLine: true), )), - Obx(() => CommonItem( - leftTitel: '位置信息'.tr, - // rightTitle: state.lockBasicInfo.value.address ?? "-", - allHeight: 80.h, - isHaveLine: false, - isHaveRightWidget: true, - rightWidget: SizedBox( - width: 300.w, - child: Text(state.lockBasicInfo.value.address ?? '无'.tr, - maxLines: 2, - overflow: TextOverflow.ellipsis, - textAlign: TextAlign.end, - style: TextStyle( - fontSize: 22.sp, - color: AppColors.darkGrayTextColor)), - ), - )), + // Obx(() => CommonItem( + // leftTitel: '位置信息'.tr, + // // rightTitle: state.lockBasicInfo.value.address ?? "-", + // allHeight: 80.h, + // isHaveLine: false, + // isHaveRightWidget: true, + // rightWidget: SizedBox( + // width: 300.w, + // child: Text(state.lockBasicInfo.value.address ?? '无'.tr, + // maxLines: 2, + // overflow: TextOverflow.ellipsis, + // textAlign: TextAlign.end, + // style: TextStyle( + // fontSize: 22.sp, + // color: AppColors.darkGrayTextColor)), + // ), + // )), /* 2024-01-12 会议确定去掉“微信二维码” by DaisyWu CommonItem( leftTitel: From de3722292e4efa841baa08b21e939850c345b3ff Mon Sep 17 00:00:00 2001 From: liyi Date: Sat, 17 May 2025 15:12:36 +0800 Subject: [PATCH 131/151] =?UTF-8?q?fix:=E5=8F=96=E6=B6=88=E6=88=AA?= =?UTF-8?q?=E5=9B=BE=E6=97=B6=E5=BF=85=E9=A1=BB=E6=8E=A5=E5=90=AC=E7=9A=84?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../imageTransmission/image_transmission_page.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/talk/starChart/views/imageTransmission/image_transmission_page.dart b/lib/talk/starChart/views/imageTransmission/image_transmission_page.dart index 3340151c..60b023bd 100644 --- a/lib/talk/starChart/views/imageTransmission/image_transmission_page.dart +++ b/lib/talk/starChart/views/imageTransmission/image_transmission_page.dart @@ -61,7 +61,7 @@ class _ImageTransmissionPageState extends State barTitle: '图传'.tr, haveBack: true, backgroundColor: AppColors.mainColor, - backAction: (){ + backAction: () { logic.udpHangUpAction(); }, ), @@ -183,10 +183,7 @@ class _ImageTransmissionPageState extends State icon: Icons.camera_alt, color: Colors.blue, onTap: () async { - if (state.talkStatus.value == - TalkStatus.answeredSuccessfully) { - await logic.captureAndSavePng(); - } + await logic.captureAndSavePng(); }, ), ], @@ -200,7 +197,10 @@ class _ImageTransmissionPageState extends State outerColor: Colors.amber.withOpacity(0.15), sliderButtonIcon: Icon(Icons.lock, color: Colors.white, size: 40.w), text: '滑动解锁', - textStyle: TextStyle(fontSize: 26.sp, color: Colors.black54, fontWeight: FontWeight.bold), + textStyle: TextStyle( + fontSize: 26.sp, + color: Colors.black54, + fontWeight: FontWeight.bold), onSubmit: () { // TODO: 实现滑动解锁逻辑 logic.remoteOpenLock(); From 47877596940a5ce75fc20c3faa402f2d20d79261 Mon Sep 17 00:00:00 2001 From: liyi Date: Sat, 17 May 2025 15:40:12 +0800 Subject: [PATCH 132/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c2bffcb6..2b0df87a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -90,6 +90,8 @@ variables: - echo $PATH - env - bash android/build.sh + - echo "=== [产物检查] build/app/outputs/flutter-apk/ 目录内容如下 ===" + - if [ -d build/app/outputs/flutter-apk/ ]; then ls -lh build/app/outputs/flutter-apk/; else echo "❌ 产物目录 build/app/outputs/flutter-apk/ 未生成!"; fi cache: paths: - app_new.version @@ -123,6 +125,8 @@ variables: - echo $PATH - env - bash ios/build.sh + - echo "=== [产物检查] build/app/outputs/flutter-ipa/ 目录内容如下 ===" + - if [ -d build/app/outputs/flutter-ipa/ ]; then ls -lh build/app/outputs/flutter-ipa/; else echo "❌ 产物目录 build/app/outputs/flutter-ipa/ 未生成!"; fi cache: paths: - app_new.version @@ -170,6 +174,8 @@ build_android: - echo $PATH - env - bash android/build.sh + - echo "=== [产物检查] build/app/outputs/flutter-apk/ 目录内容如下 ===" + - if [ -d build/app/outputs/flutter-apk/ ]; then ls -lh build/app/outputs/flutter-apk/; else echo "❌ 产物目录 build/app/outputs/flutter-apk/ 未生成!"; fi artifacts: paths: - build/app/outputs/flutter-apk/ @@ -190,6 +196,8 @@ build_ios: - echo $PATH - env - bash ios/build.sh + - echo "=== [产物检查] build/app/outputs/flutter-ipa/ 目录内容如下 ===" + - if [ -d build/app/outputs/flutter-ipa/ ]; then ls -lh build/app/outputs/flutter-ipa/; else echo "❌ 产物目录 build/app/outputs/flutter-ipa/ 未生成!"; fi artifacts: paths: - build/app/outputs/flutter-ipa @@ -212,6 +220,10 @@ create-release: - bash release_description_generator.sh - export RELEASE_DESCRIPTION="$(cat changelog.md)" - echo "${RELEASE_DESCRIPTION}" + - echo "=== [产物检查] build/app/outputs/flutter-apk/ 目录内容如下 ===" + - if [ -d build/app/outputs/flutter-apk/ ]; then ls -lh build/app/outputs/flutter-apk/; else echo "❌ 产物目录 build/app/outputs/flutter-apk/ 未生成!"; fi + - echo "=== [产物检查] build/app/outputs/flutter-ipa/ 目录内容如下 ===" + - if [ -d build/app/outputs/flutter-ipa/ ]; then ls -lh build/app/outputs/flutter-ipa/; else echo "❌ 产物目录 build/app/outputs/flutter-ipa/ 未生成!"; fi script: - export StarLock_VERSION=${CI_COMMIT_TAG#*-} - echo "Uploading StarLock-${StarLock_VERSION} packages to From 48603c7daa531e1a68d6cbedbf7a676aff82b058 Mon Sep 17 00:00:00 2001 From: liyi Date: Sat, 17 May 2025 16:32:39 +0800 Subject: [PATCH 133/151] =?UTF-8?q?fix:=E6=81=A2=E5=A4=8D=E5=8E=9F?= =?UTF-8?q?=E6=9C=89ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 91 ++------------------- android/Gemfile | 1 - android/fastlane/Fastfile | 109 ++++++------------------- ios/Gemfile | 1 - ios/fastlane/Fastfile | 69 +++++----------- scripts/bundle_install_and_auto_add.sh | 65 --------------- tag_generator.sh | 19 ++--- 7 files changed, 58 insertions(+), 297 deletions(-) delete mode 100644 scripts/bundle_install_and_auto_add.sh diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2b0df87a..64896dab 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -18,10 +18,9 @@ variables: rules: - if: $CI_COMMIT_BRANCH == "develop" - if: $CI_COMMIT_BRANCH == "release" - - if: $CI_COMMIT_BRANCH == "release_sky" - if: $CI_COMMIT_BRANCH =~ /feat_[a-zA-Z]+/ - if: $CI_COMMIT_BRANCH == "canary_release" - - if: $CI_COMMIT_TAG =~ /^(sky_)?v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]+\.[0-9]+)?$/ + - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]+\.[0-9]+)?$/ .notify_rule: tags: @@ -30,7 +29,6 @@ variables: rules: - if: $CI_COMMIT_BRANCH == "develop" - if: $CI_COMMIT_BRANCH == "release" - - if: $CI_COMMIT_BRANCH == "release_sky" - if: $CI_COMMIT_BRANCH =~ /feat_[a-zA-Z]+/ .generate_tag_rule: @@ -39,7 +37,6 @@ variables: - flutter rules: - if: $CI_COMMIT_BRANCH == "master" - - if: $CI_COMMIT_BRANCH == "master_sky" .generate_next_version_rule: tags: @@ -48,7 +45,6 @@ variables: rules: - if: $CI_COMMIT_BRANCH == "develop" - if: $CI_COMMIT_BRANCH == "release" - - if: $CI_COMMIT_BRANCH == "release_sky" - if: $CI_COMMIT_BRANCH == "canary_release" - if: $CI_COMMIT_BRANCH =~ /feat_[a-zA-Z]+/ @@ -64,34 +60,10 @@ variables: .setup_fastlane_android: extends: .build_rule before_script: - - rm -rf ~/.gem ~/.bundle vendor/bundle_android - - export GEM_HOME="$PWD/vendor/bundle_android" - - export GEM_PATH="$PWD/vendor/bundle_android" - - export PATH="$HOME/.rbenv/bin:$PATH" - - eval "$(rbenv init -)" - - rbenv global 2.7.8 - - export PATH="$HOME/.rbenv/shims:$PATH" - - which ruby # 输出当前使用的ruby路径,便于调试 - - ruby -v # 输出当前ruby版本,便于调试 - - gem sources --add https://rubygems.org || true # 保证官方源始终存在 - - 'echo -e "---\n:backtrace: false\n:bulk_threshold: 1000\n:sources:\n- https://rubygems.org\n:update_sources: true\n:verbose: true" > ~/.gemrc' - - bundle -v || gem install bundler --source https://rubygems.org/ - ls -li - export NEXT_VERSION="$(cat app_new.version)" - - bash scripts/bundle_install_and_auto_add.sh android/Gemfile vendor/bundle_android "bundle exec fastlane -v" - - gem pristine --all || true # 修复所有未编译的gem扩展 - script: - # 输出调试信息,便于后续排查环境问题 - - echo "=== DEBUG INFO (android) ===" - - which ruby - - ruby -v - - which gem - - gem -v - - echo $PATH - - env - - bash android/build.sh - - echo "=== [产物检查] build/app/outputs/flutter-apk/ 目录内容如下 ===" - - if [ -d build/app/outputs/flutter-apk/ ]; then ls -lh build/app/outputs/flutter-apk/; else echo "❌ 产物目录 build/app/outputs/flutter-apk/ 未生成!"; fi + # - flutter pub get + - bundle install --gemfile android/Gemfile --quiet cache: paths: - app_new.version @@ -99,34 +71,10 @@ variables: .setup_fastlane_ios: extends: .build_rule before_script: - - rm -rf ~/.gem ~/.bundle vendor/bundle_ios - - export GEM_HOME="$PWD/vendor/bundle_ios" - - export GEM_PATH="$PWD/vendor/bundle_ios" - - export PATH="$HOME/.rbenv/bin:$PATH" - - eval "$(rbenv init -)" - - rbenv global 2.7.8 - - export PATH="$HOME/.rbenv/shims:$PATH" - - which ruby # 输出当前使用的ruby路径,便于调试 - - ruby -v # 输出当前ruby版本,便于调试 - - gem sources --add https://rubygems.org || true # 保证官方源始终存在 - - 'echo -e "---\n:backtrace: false\n:bulk_threshold: 1000\n:sources:\n- https://rubygems.org\n:update_sources: true\n:verbose: true" > ~/.gemrc' - - bundle -v || gem install bundler --source https://rubygems.org/ - ls -li - export NEXT_VERSION="$(cat app_new.version)" - - bash scripts/bundle_install_and_auto_add.sh ios/Gemfile vendor/bundle_ios "bundle exec fastlane -v" - - gem pristine --all || true # 修复所有未编译的gem扩展 - script: - # 输出调试信息,便于后续排查环境问题 - - echo "=== DEBUG INFO (ios) ===" - - which ruby - - ruby -v - - which gem - - gem -v - - echo $PATH - - env - - bash ios/build.sh - - echo "=== [产物检查] build/app/outputs/flutter-ipa/ 目录内容如下 ===" - - if [ -d build/app/outputs/flutter-ipa/ ]; then ls -lh build/app/outputs/flutter-ipa/; else echo "❌ 产物目录 build/app/outputs/flutter-ipa/ 未生成!"; fi + # - flutter pub get + - bundle install --gemfile ios/Gemfile --quiet cache: paths: - app_new.version @@ -165,17 +113,7 @@ generate_next_version: build_android: stage: build-artifacts extends: .setup_fastlane_android - script: - - echo "=== DEBUG INFO (android) ===" - - which ruby - - ruby -v - - which gem - - gem -v - - echo $PATH - - env - - bash android/build.sh - - echo "=== [产物检查] build/app/outputs/flutter-apk/ 目录内容如下 ===" - - if [ -d build/app/outputs/flutter-apk/ ]; then ls -lh build/app/outputs/flutter-apk/; else echo "❌ 产物目录 build/app/outputs/flutter-apk/ 未生成!"; fi + script: bash android/build.sh artifacts: paths: - build/app/outputs/flutter-apk/ @@ -183,21 +121,8 @@ build_android: build_ios: stage: build-artifacts extends: .setup_fastlane_ios - needs: - - build_android - dependencies: - - build_android script: - - echo "=== DEBUG INFO (ios) ===" - - which ruby - - ruby -v - - which gem - - gem -v - - echo $PATH - - env - bash ios/build.sh - - echo "=== [产物检查] build/app/outputs/flutter-ipa/ 目录内容如下 ===" - - if [ -d build/app/outputs/flutter-ipa/ ]; then ls -lh build/app/outputs/flutter-ipa/; else echo "❌ 产物目录 build/app/outputs/flutter-ipa/ 未生成!"; fi artifacts: paths: - build/app/outputs/flutter-ipa @@ -220,10 +145,6 @@ create-release: - bash release_description_generator.sh - export RELEASE_DESCRIPTION="$(cat changelog.md)" - echo "${RELEASE_DESCRIPTION}" - - echo "=== [产物检查] build/app/outputs/flutter-apk/ 目录内容如下 ===" - - if [ -d build/app/outputs/flutter-apk/ ]; then ls -lh build/app/outputs/flutter-apk/; else echo "❌ 产物目录 build/app/outputs/flutter-apk/ 未生成!"; fi - - echo "=== [产物检查] build/app/outputs/flutter-ipa/ 目录内容如下 ===" - - if [ -d build/app/outputs/flutter-ipa/ ]; then ls -lh build/app/outputs/flutter-ipa/; else echo "❌ 产物目录 build/app/outputs/flutter-ipa/ 未生成!"; fi script: - export StarLock_VERSION=${CI_COMMIT_TAG#*-} - echo "Uploading StarLock-${StarLock_VERSION} packages to diff --git a/android/Gemfile b/android/Gemfile index 5f5b8322..cdd3a6b3 100644 --- a/android/Gemfile +++ b/android/Gemfile @@ -1,7 +1,6 @@ source "https://rubygems.org" gem "fastlane" -gem 'nkf', '0.2.0' plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/android/fastlane/Fastfile b/android/fastlane/Fastfile index 00e3b286..a99f62f0 100644 --- a/android/fastlane/Fastfile +++ b/android/fastlane/Fastfile @@ -44,29 +44,22 @@ platform :android do print_header '🏁 Before All' print_log $current_branch print_log $current_tag + print_log $path_file_preview_apk_default + print_log $path_file_release_apk_default + print_log $path_file_preview_apk_copy + print_log $path_file_release_apk_copy Dir.chdir "../.." do sh('pwd') end - # RubyGems/bundle环境相关日志 - sh('which ruby') - sh('ruby -v') - sh('which gem') - sh('gem -v') - sh('gem sources -l') - sh('which bundle') - sh('bundle -v') - sh('bundle config') - sh('env | grep GEM || true') - sh('env | grep BUNDLE || true') - sh('env | grep CI_ || true') end desc "Submit a new Beta Build to Pgy Beta" lane :beta do |options| + flavor = options[:flavor] env = options[:env] - flavor = 'sky' + UI.user_error!("flavor is required") unless flavor UI.user_error!("env is required") unless env - print_log "build sky on #{env}" + print_log "build #{flavor} on #{env}" build_number = Time.now.strftime("%Y%m%d%H") print_log "BuildNo #{build_number}" build_version = $next_version @@ -76,28 +69,11 @@ platform :android do print_log "last_git_commit_short_hash #{short_hash}" remove_zone_pre_build(zone:"com") Dir.chdir "../.." do - sh('pwd') - sh('ls -lh') - sh('ls -lh build/app/outputs/flutter-apk/ || true') - sh('ls -lh build/app/outputs/bundle/ || true') - sh("flutter","pub","get") - end - Dir.chdir "../.." do - sh('pwd') - sh('ls -lh') - sh('ls -lh build/app/outputs/flutter-apk/ || true') - sh('ls -lh build/app/outputs/bundle/ || true') - sh("flutter", "build", "apk", "--no-tree-shake-icons", "--release", "--flavor", "sky_#{env}", "-t", "lib/main_sky_#{env}.dart", "--build-number=#{build_number}", "--build-name=#{build_version}") - sh('ls -lh build/app/outputs/flutter-apk/ || true') - sh('ls -lh build/app/outputs/bundle/ || true') - end - old_file_path = File.join($path_apk_output_dir, "app-sky_#{env}-release.apk") - new_file_path = File.join($path_apk_output_dir, "starlock-sky-preview-#{build_version}.apk") - print_log "old_file_path: #{old_file_path}" - print_log "new_file_path: #{new_file_path}" - unless File.exist?(old_file_path) - UI.user_error!("❌ 产物 #{old_file_path} 未生成,构建失败!") + sh("flutter","pub","get") + sh("flutter", "build", "apk", "--no-tree-shake-icons", "--release", "--flavor", "#{flavor}_#{env}", "-t", "lib/main_#{flavor}_#{env}.dart", "--build-number=#{build_number}", "--build-name=#{build_version}") end + old_file_path = File.join($path_apk_output_dir, "app-#{flavor}_#{env}-release.apk") + new_file_path = File.join($path_apk_output_dir, "starlock-#{flavor}-preview-#{build_version}.apk") File.rename(old_file_path, new_file_path) logs = changelog_from_git_commits( pretty: '- %s (%cn)', @@ -109,81 +85,46 @@ platform :android do desc "Build & upload a new version to Gitlab Release" lane :release_apk do |options| - flavor = 'sky' - print_log "build flavor for: sky" + flavor = options[:flavor] + UI.user_error!("flavor is required") unless flavor + print_log "build flavor for: #{flavor}" build_number = Time.now.strftime("%Y%m%d%H") print_log "BuildNo #{build_number}" - build_version = $current_tag.match(/^(sky_)?v(\d+\.\d+\.\d+)/).captures.last + build_version = $current_tag.match(/^v(\d+\.\d+\.\d+)/).captures[0] print_log "buildVersion #{build_version}" commit_hash = last_git_commit short_hash = commit_hash[:abbreviated_commit_hash] print_log "last_git_commit_short_hash #{short_hash}" remove_zone_pre_build(zone:"com") Dir.chdir "../.." do - sh('pwd') - sh('ls -lh') - sh('ls -lh build/app/outputs/flutter-apk/ || true') - sh('ls -lh build/app/outputs/bundle/ || true') sh("flutter","pub","get") + sh("flutter", "build", "apk", "--no-tree-shake-icons", "--release", "--flavor", "#{flavor}", "-t", "lib/main_#{flavor}_lite.dart", "--build-number=#{build_number}", "--build-name=#{build_version}") end - Dir.chdir "../.." do - sh('pwd') - sh('ls -lh') - sh('ls -lh build/app/outputs/flutter-apk/ || true') - sh('ls -lh build/app/outputs/bundle/ || true') - sh("flutter", "build", "apk", "--no-tree-shake-icons", "--release", "--flavor", "sky", "-t", "lib/main_sky_lite.dart", "--build-number=#{build_number}", "--build-name=#{build_version}") - sh('ls -lh build/app/outputs/flutter-apk/ || true') - sh('ls -lh build/app/outputs/bundle/ || true') - end - old_apk_file_path = File.join($path_apk_output_dir, "app-sky-release.apk") - new_apk_file_path = File.join($path_apk_output_dir, "starlock-sky-release-"+$current_tag+".apk") - print_log "old_apk_file_path: #{old_apk_file_path}" - print_log "new_apk_file_path: #{new_apk_file_path}" - unless File.exist?(old_apk_file_path) - UI.user_error!("❌ 产物 #{old_apk_file_path} 未生成,构建失败!") - end + old_apk_file_path = File.join($path_apk_output_dir, "app-#{flavor}-release.apk") + new_apk_file_path = File.join($path_apk_output_dir, "starlock-#{flavor}-release-"+$current_tag+".apk") File.rename(old_apk_file_path, new_apk_file_path) end desc "Build & upload a new version to Gitlab Release" lane :release_bundle do |options| - flavor = 'sky' - print_log "build flavor for: sky" + flavor = options[:flavor] + UI.user_error!("flavor is required") unless flavor + print_log "build flavor for: #{flavor}" build_number = Time.now.strftime("%Y%m%d%H") print_log "BuildNo #{build_number}" - build_version = $current_tag.match(/^(sky_)?v(\d+\.\d+\.\d+)/).captures.last + build_version = $current_tag.match(/^v(\d+\.\d+\.\d+)/).captures[0] print_log "buildVersion #{build_version}" commit_hash = last_git_commit short_hash = commit_hash[:abbreviated_commit_hash] print_log "last_git_commit_short_hash #{short_hash}" remove_zone_pre_build(zone:"cn") Dir.chdir "../.." do - sh('pwd') - sh('ls -lh') - sh('ls -lh build/app/outputs/flutter-apk/ || true') - sh('ls -lh build/app/outputs/bundle/ || true') sh("flutter","pub","get") + sh("flutter", "build", "appbundle", "--no-tree-shake-icons", "--release", "--flavor", "#{flavor}", "-t", "lib/main_#{flavor}_lite.dart", "--build-number=#{build_number}", "--build-name=#{build_version}") end - Dir.chdir "../.." do - sh('pwd') - sh('ls -lh') - sh('ls -lh build/app/outputs/flutter-apk/ || true') - sh('ls -lh build/app/outputs/bundle/ || true') - sh("flutter", "build", "appbundle", "--no-tree-shake-icons", "--release", "--flavor", "sky", "-t", "lib/main_sky_lite.dart", "--build-number=#{build_number}", "--build-name=#{build_version}") - sh('ls -lh build/app/outputs/flutter-apk/ || true') - sh('ls -lh build/app/outputs/bundle/ || true') - end - old_bundle_file_path = File.join($path_bundle_output_dir , "/skyRelease/app-sky-release.aab") - new_bundle_file_path = File.join($path_bundle_output_dir , "/skyRelease/starlock-sky-release-"+$current_tag+".aab") - print_log "old_bundle_file_path: #{old_bundle_file_path}" - print_log "new_bundle_file_path: #{new_bundle_file_path}" - unless File.exist?(old_bundle_file_path) - UI.user_error!("❌ 产物 #{old_bundle_file_path} 未生成,构建失败!") - end + old_bundle_file_path = File.join($path_bundle_output_dir , "/#{flavor}Release/app-#{flavor}-release.aab") + new_bundle_file_path = File.join($path_bundle_output_dir , "/#{flavor}Release/starlock-#{flavor}-release-"+$current_tag+".aab") File.rename(old_bundle_file_path, new_bundle_file_path) - unless File.exist?(new_bundle_file_path) - UI.user_error!("❌ 产物 #{new_bundle_file_path} 未生成,构建失败!") - end sh('cp',new_bundle_file_path,$path_apk_output_dir) end diff --git a/ios/Gemfile b/ios/Gemfile index cb7538c4..21cb5dfc 100644 --- a/ios/Gemfile +++ b/ios/Gemfile @@ -5,4 +5,3 @@ gem 'cocoapods', '1.14.3' gem 'public_suffix', '~> 4.0' plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') eval_gemfile(plugins_path) if File.exist?(plugins_path) -gem 'nkf', '0.2.0' diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index dc7bc668..3591770d 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -68,55 +68,38 @@ platform :ios do Dir.chdir "../.." do sh('pwd') end - # RubyGems/bundle环境相关日志 - sh('which ruby') - sh('ruby -v') - sh('which gem') - sh('gem -v') - sh('gem sources -l') - sh('which bundle') - sh('bundle -v') - sh('bundle config') - sh('env | grep GEM || true') - sh('env | grep BUNDLE || true') - sh('env | grep CI_ || true') end desc "Build & Deliver to Pgy" lane :beta do |options| + flavor = options[:flavor] env = options[:env] - flavor = 'sky' + UI.user_error!("flavor is required") unless flavor UI.user_error!("env is required") unless env - print_log "build sky on #{env}" + print_log "build #{flavor} on #{env}" build_number = Time.now.strftime("%Y%m%d%H%M") print_log "Build Commits #{build_number}" - build_version = $next_version + build_version = $next_version # Time.now.strftime("%Y%m%d%H%M%S") print_log "build_version #{build_version}" commit_hash = last_git_commit short_hash = commit_hash[:abbreviated_commit_hash] print_log "last_git_commit_short_hash #{short_hash}" remove_zone_pre_build(zone:"com") Dir.chdir "../.." do - sh('pwd') - sh('ls -lh') - sh('ls -lh build/app/outputs/flutter-ipa/ || true') sh("flutter","pub","get") end Dir.chdir ".." do sh("bundle", "exec" ,"pod", "install") end Dir.chdir "../.." do - sh('pwd') - sh('ls -lh') - sh('ls -lh build/app/outputs/flutter-ipa/ || true') - sh("flutter", "build", "ios", "--no-tree-shake-icons", "--no-codesign", "--release", "--flavor", "sky", "-t", "lib/main_sky_#{env}.dart", "--build-number=#{build_number}", "--build-name=#{build_version}") - sh('ls -lh build/app/outputs/flutter-ipa/ || true') + sh("flutter", "build", "ios", "--no-tree-shake-icons", "--no-codesign", "--release", "--flavor", "#{flavor}", "-t", "lib/main_#{flavor}_#{env}.dart", "--build-number=#{build_number}", "--build-name=#{build_version}") end - gym_scheme_preview="sky" - print_log "gym_scheme_preview sky" - gym_configuration = "#{env}-release-sky" + #set_bundle_short_version('./Runner/Info.plist', $current_branch) + gym_scheme_preview="#{flavor}" + print_log "gym_scheme_preview #{gym_scheme_preview}" + gym_configuration = "#{env}-release-#{flavor}" print_log "gym_configuration #{gym_configuration}" - ipa_default_filename = "starlock-sky-preview-#{build_version}.ipa" + ipa_default_filename = "starlock-#{flavor}-preview-#{build_version}.ipa" print_log "ipa_default_filename #{ipa_default_filename}" gym( scheme: gym_scheme_preview, @@ -127,6 +110,8 @@ platform :ios do export_method: "ad-hoc", export_options: { provisioningProfiles: { + "com.xhjcn.lock.dev" => "Adhoc_com.xhjcn.lock.dev.mobileprovision", + "com.xhjcn.lock.pre" => "Adhoc_com.xhjcn.lock.pre.mobileprovision", "com.skychip.lock.dev" => "Adhoc_com.skychip.lock.dev.mobileprovision", "com.skychip.lock.pre" => "Adhoc_com.skychip.lock.pre.mobileprovision", } @@ -142,42 +127,31 @@ platform :ios do desc "Build & Deliver to App Store Connect" lane :release_ipa do |options| - flavor = 'sky' - print_log "build flavor: sky" + flavor = options[:flavor] + UI.user_error!("flavor is required") unless flavor + print_log "build flavor: #{flavor}" build_number = Time.now.strftime("%Y%m%d%H%M") print_log "Build Commits #{build_number}" - m = $current_tag.match(/^(sky_)?v(\d+\.\d+\.\d+)/) - if m - build_version = m.captures.last - else - UI.user_error!("Tag格式不正确,无法提取版本号: #{$current_tag}") - end + build_version = $current_tag.match(/^v(\d+\.\d+\.\d+)/).captures[0] print_log "build_version #{build_version}" commit_hash = last_git_commit short_hash = commit_hash[:abbreviated_commit_hash] print_log "last_git_commit_short_hash #{short_hash}" remove_zone_pre_build(zone:"com") Dir.chdir "../.." do - sh('pwd') - sh('ls -lh') - sh('ls -lh build/app/outputs/flutter-ipa/ || true') sh("flutter","pub","get") end Dir.chdir ".." do sh("bundle", "exec" ,"pod", "install") end Dir.chdir "../.." do - sh('pwd') - sh('ls -lh') - sh('ls -lh build/app/outputs/flutter-ipa/ || true') - sh("flutter", "build", "ios", "--no-tree-shake-icons", "--no-codesign", "--release", "--flavor", "sky", "-t", "lib/main_sky_lite.dart", "--build-number=#{build_number}", "--build-name=#{build_version}") - sh('ls -lh build/app/outputs/flutter-ipa/ || true') + sh("flutter", "build", "ios", "--no-tree-shake-icons", "--no-codesign", "--release", "--flavor", "#{flavor}", "-t", "lib/main_#{flavor}_lite.dart", "--build-number=#{build_number}", "--build-name=#{build_version}") end - gym_scheme_release="sky" - print_log "gym_scheme_release sky" - gym_configuration = "Release-sky" + gym_scheme_release="#{flavor}" + print_log "gym_scheme_release #{gym_scheme_release}" + gym_configuration = "Release-#{flavor}" print_log "gym_configuration #{gym_configuration}" - ipa_default_filename = "starlock-sky-release-"+$current_tag+".ipa" + ipa_default_filename = "starlock-#{flavor}-release-"+$current_tag+".ipa" print_log "ipa_default_filename #{ipa_default_filename}" gym( scheme: gym_scheme_release, @@ -188,6 +162,7 @@ platform :ios do export_method: "app-store", export_options: { provisioningProfiles: { + "com.xhjcn.lock" => "Appstore_com.xhjcn.lock.mobileprovision", "com.skychip.lock" => "Appstore_com.skychip.lock.mobileprovision", } } diff --git a/scripts/bundle_install_and_auto_add.sh b/scripts/bundle_install_and_auto_add.sh deleted file mode 100644 index 84fd6d2d..00000000 --- a/scripts/bundle_install_and_auto_add.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/bash -# 用法: bash scripts/bundle_install_and_auto_add.sh -# 例如: bash scripts/bundle_install_and_auto_add.sh ios/Gemfile vendor/bundle_ios "bundle exec fastlane -v" - -GEMFILE_PATH="$1" -BUNDLE_PATH="$2" -MAIN_CMD="$3" - -if [ -z "$GEMFILE_PATH" ] || [ -z "$BUNDLE_PATH" ] || [ -z "$MAIN_CMD" ]; then - echo "用法: $0 " - exit 1 -fi - -# 保证rubygems.org官方源始终存在(开头) -gem sources --add https://rubygems.org || true - -max_auto_add=3 -add_count=0 -success=0 - -# 1. 检查并自动补全Gemfile缺失依赖 -while [ $add_count -lt $max_auto_add ]; do - echo "[INFO] 第$((add_count+1))次尝试运行主命令: $MAIN_CMD (仅捕获缺失gem)" - $MAIN_CMD > bundle_missing_gem.log 2>&1 && success=1 && break - missing_gem=$(grep -o "Could not find [^ ]*" bundle_missing_gem.log | awk '{print $4}' | head -n1) - if [ -n "$missing_gem" ]; then - if ! grep -q "gem '$missing_gem'" "$GEMFILE_PATH"; then - echo "gem '$missing_gem'" >> "$GEMFILE_PATH" - echo "[AUTO] Gemfile已自动补全: $missing_gem" - else - echo "[WARN] Gemfile已包含 $missing_gem,但依然缺失,可能是平台或缓存问题" - fi - else - echo "[INFO] 未检测到缺失gem,或主命令已成功。" - break - fi - add_count=$((add_count+1)) - sleep 2 - echo "[WARN] 第$add_count 次自动补全后重试..." -done - -if [ $success -eq 1 ]; then - echo "[SUCCESS] 所有依赖已补全,主命令执行成功。" - # 再次保证rubygems.org官方源始终存在(结尾) - gem sources --add https://rubygems.org || true - exit 0 -fi - -# 2. 只用 rubygems.org 官方源进行 bundle install -bundle config mirror.https://rubygems.org https://rubygems.org -bundle config set --local path "$BUNDLE_PATH" -bundle install --gemfile "$GEMFILE_PATH" -if [ $? -eq 0 ]; then - echo "[SUCCESS] 官方源 bundle install 成功" -else - echo "[FATAL] 官方源 bundle install 失败,请检查网络或Gemfile配置。" - exit 2 -fi - -# 3. 最后再执行一次主命令校验 -$MAIN_CMD || { echo "[FATAL] 主命令依然失败,请人工检查Gemfile和依赖环境。"; exit 3; } -# 再次保证rubygems.org官方源始终存在(结尾) -gem sources --add https://rubygems.org || true -echo "[SUCCESS] 所有依赖已补全,主命令执行成功。" -exit 0 \ No newline at end of file diff --git a/tag_generator.sh b/tag_generator.sh index 35529ba2..27e39182 100755 --- a/tag_generator.sh +++ b/tag_generator.sh @@ -15,17 +15,15 @@ if [ "$tags_length" -lt 1 ]; then next_tag="v1.0.0" else newest_tag=$(echo "$tags" | head -n 1) - # 去除已有的sky_前缀,防止重复 - base_tag=${newest_tag#sky_} - IFS='.' read -r major minor patch <<< "$base_tag" + IFS='.' read -r major minor patch <<< "$newest_tag" major="${major#v}" compare_json="" if [[ "$1" == "generate_tag" ]];then - echo "generate_tag:$newest_tag-to-master_sky\n" - compare_json=$(curl -s --header "PRIVATE-TOKEN: $TOKEN" "$URL/projects/$PROJECT_ID/repository/compare?from=$newest_tag&to=master_sky") + echo "generate_tag:$newest_tag-to-master\n" + compare_json=$(curl -s --header "PRIVATE-TOKEN: $TOKEN" "$URL/projects/$PROJECT_ID/repository/compare?from=$newest_tag&to=master") elif [[ "$1" == "generate_version" ]]; then - echo "generate_version:master_sky-to-$CI_COMMIT_BRANCH\n" - compare_json=$(curl -s --header "PRIVATE-TOKEN: $TOKEN" "$URL/projects/$PROJECT_ID/repository/compare?from=master_sky&to=$CI_COMMIT_BRANCH") + echo "generate_version:master-to-$CI_COMMIT_BRANCH\n" + compare_json=$(curl -s --header "PRIVATE-TOKEN: $TOKEN" "$URL/projects/$PROJECT_ID/repository/compare?from=master&to=$CI_COMMIT_BRANCH") fi echo "compare_json:$compare_json\n" new_patch=$patch @@ -47,13 +45,6 @@ else done < <(echo "$compare_json" | jq -c '.commits[] | {id: .id, message: .message}') next_tag="v$major.$new_minor.$new_patch" fi - -# 生成新tag名时,若在master_sky分支,加前缀sky_ -branch_name="$CI_COMMIT_BRANCH" -if [[ "$branch_name" == "master_sky" ]]; then - next_tag="sky_${next_tag}" -fi - echo "New Tag:$newest_tag;New version: $next_tag;command: $1" if [[ "$1" == "generate_tag" ]];then if [ "$next_tag" == "$newest_tag" ]; then From 78ad994cc4b59ca5801abf849c898584b89771ab Mon Sep 17 00:00:00 2001 From: liyi Date: Sat, 17 May 2025 17:10:30 +0800 Subject: [PATCH 134/151] =?UTF-8?q?fix:=E6=81=A2=E5=A4=8D=E5=8E=9F?= =?UTF-8?q?=E6=9C=89ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/build.sh | 38 +++++++++++--------------------------- ios/build.sh | 32 +++++++++----------------------- 2 files changed, 20 insertions(+), 50 deletions(-) diff --git a/android/build.sh b/android/build.sh index 10618af0..4b598959 100755 --- a/android/build.sh +++ b/android/build.sh @@ -8,41 +8,25 @@ export ENV_BUILD_WORKSPACE=${CI_PROJECT_DIR} echo "GITLAB_WORKSPACE: ${CI_PROJECT_DIR}" cd ${CI_PROJECT_DIR}/android echo "ENV_BUILD_TAG:${ENV_BUILD_TAG},ENV_BUILD_BRANCH:${ENV_BUILD_BRANCH}" -regex='^(sky_)?v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]+\.[0-9]+)?$' +regex='^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]+\.[0-9]+)?$' if [[ "${ENV_BUILD_BRANCH}" == "canary_release" ]]; then echo "===build canary_release: ${NEXT_VERSION}" export ENV_BUILD_TAG=${NEXT_VERSION} - bundle exec fastlane release_apk flavor:xhj --verbose || { echo "[FATAL] fastlane release_apk xhj 失败"; exit 10; } - bundle exec fastlane release_apk flavor:sky --verbose || { echo "[FATAL] fastlane release_apk sky 失败"; exit 11; } + bundle exec fastlane release_apk flavor:xhj --verbose + bundle exec fastlane release_apk flavor:sky --verbose elif [[ $ENV_BUILD_TAG =~ $regex ]]; then echo "===build release===$ENV_BUILD_TAG" - bundle exec fastlane release_apk flavor:xhj --verbose || { echo "[FATAL] fastlane release_apk xhj 失败"; exit 10; } - bundle exec fastlane release_apk flavor:sky --verbose || { echo "[FATAL] fastlane release_apk sky 失败"; exit 11; } - bundle exec fastlane release_bundle flavor:xhj_bundle --verbose || { echo "[FATAL] fastlane release_bundle xhj_bundle 失败"; exit 12; } - bundle exec fastlane release_bundle flavor:sky --verbose || { echo "[FATAL] fastlane release_bundle sky 失败"; exit 13; } - echo "=== fastlane lane 结束,检查产物目录(前) ===" - ls -lh build/app/outputs/flutter-apk/ || true - ls -lh build/app/outputs/bundle/ || true - if [ ! -d build/app/outputs/flutter-apk/ ]; then - echo "❌ 产物目录 build/app/outputs/flutter-apk/ 未生成,构建失败!" - echo "=== 产物目录消失时,build/app/outputs/ 内容如下 ===" - ls -lh build/app/outputs/ || true - ls -lh build/app/outputs/bundle/ || true - exit 99 - fi - echo "=== 产物目录检查通过,最终内容如下 ===" - ls -l build/app/outputs/flutter-apk/ + bundle exec fastlane release_apk flavor:xhj --verbose + bundle exec fastlane release_apk flavor:sky --verbose + bundle exec fastlane release_bundle flavor:xhj_bundle --verbose + bundle exec fastlane release_bundle flavor:sky --verbose elif [[ "${ENV_BUILD_BRANCH}" == "develop" ]]; then echo "===build dev===${NEXT_VERSION}" - bundle exec fastlane beta flavor:xhj env:dev --verbose || { echo "[FATAL] fastlane beta xhj dev 失败"; exit 20; } - bundle exec fastlane beta flavor:sky env:dev --verbose || { echo "[FATAL] fastlane beta sky dev 失败"; exit 21; } + bundle exec fastlane beta flavor:xhj env:dev --verbose + bundle exec fastlane beta flavor:sky env:dev --verbose elif [[ "${ENV_BUILD_BRANCH}" == "release" ]] || [[ "${ENV_BUILD_BRANCH}" == "feat_devops" ]] ; then echo "===build pre===${NEXT_VERSION}" - bundle exec fastlane beta flavor:xhj env:pre --verbose || { echo "[FATAL] fastlane beta xhj pre 失败"; exit 30; } - bundle exec fastlane beta flavor:sky env:pre --verbose || { echo "[FATAL] fastlane beta sky pre 失败"; exit 31; } -elif [[ "${ENV_BUILD_BRANCH}" == "release_sky" ]]; then - echo "===build release_sky===${NEXT_VERSION}" - bundle exec fastlane release_apk flavor:sky --verbose || { echo "[FATAL] fastlane release_apk sky 失败"; exit 40; } - bundle exec fastlane release_bundle flavor:sky --verbose || { echo "[FATAL] fastlane release_bundle sky 失败"; exit 41; } + bundle exec fastlane beta flavor:xhj env:pre --verbose + bundle exec fastlane beta flavor:sky env:pre --verbose fi exit 0 \ No newline at end of file diff --git a/ios/build.sh b/ios/build.sh index e7235771..a8a977bf 100755 --- a/ios/build.sh +++ b/ios/build.sh @@ -9,37 +9,23 @@ echo "GITLAB_WORKSPACE: ${CI_PROJECT_DIR}" cd ${CI_PROJECT_DIR}/ios #bundle exec pod install echo "ENV_BUILD_TAG:${ENV_BUILD_TAG},ENV_BUILD_BRANCH:${ENV_BUILD_BRANCH}" -regex='^(sky_)?v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]+\.[0-9]+)?$' - -# ==== 调试输出,确认环境和依赖 ==== -echo "=== FASTLANE/GEM/ENV DEBUG ===" -which fastlane -fastlane -v -which bundle -bundle -v -echo $PATH -gem list | grep fastlane -gem list | grep digest-crc -# ==== END DEBUG ==== - +regex='^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]+\.[0-9]+)?$' if [[ "${ENV_BUILD_BRANCH}" == "canary_release" ]]; then echo "===build canary_release: ${NEXT_VERSION}" export ENV_BUILD_TAG=${NEXT_VERSION} - bundle exec fastlane release_ipa flavor:xhj --verbose || { echo "[FATAL] fastlane release_ipa xhj 失败"; exit 10; } - bundle exec fastlane release_ipa flavor:sky --verbose || { echo "[FATAL] fastlane release_ipa sky 失败"; exit 11; } + bundle exec fastlane release_ipa flavor:xhj --verbose + bundle exec fastlane release_ipa flavor:sky --verbose elif [[ $ENV_BUILD_TAG =~ $regex ]]; then echo "===build release===$ENV_BUILD_TAG" - # 无论tag前缀,均构建xhj和sky的ipa - bundle exec fastlane release_ipa flavor:xhj --verbose || { echo "[FATAL] fastlane release_ipa xhj 失败"; exit 10; } - bundle exec fastlane release_ipa flavor:sky --verbose || { echo "[FATAL] fastlane release_ipa sky 失败"; exit 11; } - ls -l build/app/outputs/flutter-ipa/ + bundle exec fastlane release_ipa flavor:xhj --verbose + bundle exec fastlane release_ipa flavor:sky --verbose elif [[ "${ENV_BUILD_BRANCH}" == "develop" ]]; then echo "===build dev===${NEXT_VERSION}" - bundle exec fastlane beta flavor:xhj env:Dev --verbose || { echo "[FATAL] fastlane beta xhj Dev 失败"; exit 20; } - bundle exec fastlane beta flavor:sky env:Dev --verbose || { echo "[FATAL] fastlane beta sky Dev 失败"; exit 21; } + bundle exec fastlane beta flavor:xhj env:Dev --verbose + bundle exec fastlane beta flavor:sky env:Dev --verbose elif [[ "${ENV_BUILD_BRANCH}" == "release" ]] || [[ "${ENV_BUILD_BRANCH}" == "feat_devops" ]] ; then echo "===build pre===${NEXT_VERSION}" - bundle exec fastlane beta flavor:xhj env:Pre --verbose || { echo "[FATAL] fastlane beta xhj Pre 失败"; exit 30; } - bundle exec fastlane beta flavor:sky env:Pre --verbose || { echo "[FATAL] fastlane beta sky Pre 失败"; exit 31; } + bundle exec fastlane beta flavor:xhj env:Pre --verbose + bundle exec fastlane beta flavor:sky env:Pre --verbose fi exit 0 \ No newline at end of file From da85d65fa2b0356422c4d3726d2749c53c2035e1 Mon Sep 17 00:00:00 2001 From: liyi Date: Sat, 17 May 2025 17:36:57 +0800 Subject: [PATCH 135/151] =?UTF-8?q?fix:=E6=81=A2=E5=A4=8D=E5=8E=9F?= =?UTF-8?q?=E6=9C=89ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 64896dab..bb562cf0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -60,10 +60,11 @@ variables: .setup_fastlane_android: extends: .build_rule before_script: - - ls -li + - gem install bundler + - bundle clean --force + - bundle install --gemfile android/Gemfile --quiet - export NEXT_VERSION="$(cat app_new.version)" # - flutter pub get - - bundle install --gemfile android/Gemfile --quiet cache: paths: - app_new.version From eeff5f0665513b31934d65a2eb35641b17b9b30f Mon Sep 17 00:00:00 2001 From: liyi Date: Sat, 17 May 2025 17:45:53 +0800 Subject: [PATCH 136/151] =?UTF-8?q?fix:=E6=81=A2=E5=A4=8D=E5=8E=9F?= =?UTF-8?q?=E6=9C=89ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bb562cf0..64896dab 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -60,11 +60,10 @@ variables: .setup_fastlane_android: extends: .build_rule before_script: - - gem install bundler - - bundle clean --force - - bundle install --gemfile android/Gemfile --quiet + - ls -li - export NEXT_VERSION="$(cat app_new.version)" # - flutter pub get + - bundle install --gemfile android/Gemfile --quiet cache: paths: - app_new.version From d046167518aa5b32ea006ff9660c5064d650476c Mon Sep 17 00:00:00 2001 From: liyi Date: Sat, 17 May 2025 17:50:15 +0800 Subject: [PATCH 137/151] =?UTF-8?q?fix:=E6=81=A2=E5=A4=8D=E5=8E=9F?= =?UTF-8?q?=E6=9C=89ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/Gemfile.lock | 221 ------------------------------------------- 1 file changed, 221 deletions(-) delete mode 100644 android/Gemfile.lock diff --git a/android/Gemfile.lock b/android/Gemfile.lock deleted file mode 100644 index 554a5952..00000000 --- a/android/Gemfile.lock +++ /dev/null @@ -1,221 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - CFPropertyList (3.0.7) - base64 - nkf - rexml - addressable (2.8.7) - public_suffix (>= 2.0.2, < 7.0) - artifactory (3.0.17) - atomos (0.1.3) - aws-eventstream (1.3.0) - aws-partitions (1.979.0) - aws-sdk-core (3.209.1) - aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.9) - jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.94.0) - aws-sdk-core (~> 3, >= 3.207.0) - aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.166.0) - aws-sdk-core (~> 3, >= 3.207.0) - aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.5) - aws-sigv4 (1.10.0) - aws-eventstream (~> 1, >= 1.0.2) - babosa (1.0.4) - base64 (0.2.0) - claide (1.1.0) - colored (1.2) - colored2 (3.1.2) - commander (4.6.0) - highline (~> 2.0.0) - declarative (0.0.20) - digest-crc (0.6.5) - rake (>= 12.0.0, < 14.0.0) - domain_name (0.5.20190701) - unf (>= 0.0.5, < 1.0.0) - dotenv (2.8.1) - emoji_regex (3.2.3) - excon (0.109.0) - faraday (1.10.4) - faraday-em_http (~> 1.0) - faraday-em_synchrony (~> 1.0) - faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0) - faraday-multipart (~> 1.0) - faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.0) - faraday-patron (~> 1.0) - faraday-rack (~> 1.0) - faraday-retry (~> 1.0) - ruby2_keywords (>= 0.0.4) - faraday-cookie_jar (0.0.7) - faraday (>= 0.8.0) - http-cookie (~> 1.0.0) - faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.0) - faraday-excon (1.1.0) - faraday-httpclient (1.0.1) - faraday-multipart (1.0.4) - multipart-post (~> 2) - faraday-net_http (1.0.2) - faraday-net_http_persistent (1.2.0) - faraday-patron (1.0.0) - faraday-rack (1.0.0) - faraday-retry (1.0.3) - faraday_middleware (1.2.1) - faraday (~> 1.0) - fastimage (2.3.1) - fastlane (2.222.0) - CFPropertyList (>= 2.3, < 4.0.0) - addressable (>= 2.8, < 3.0.0) - artifactory (~> 3.0) - aws-sdk-s3 (~> 1.0) - babosa (>= 1.0.3, < 2.0.0) - bundler (>= 1.12.0, < 3.0.0) - colored (~> 1.2) - commander (~> 4.6) - dotenv (>= 2.1.1, < 3.0.0) - emoji_regex (>= 0.1, < 4.0) - excon (>= 0.71.0, < 1.0.0) - faraday (~> 1.0) - faraday-cookie_jar (~> 0.0.6) - faraday_middleware (~> 1.0) - fastimage (>= 2.1.0, < 3.0.0) - gh_inspector (>= 1.1.2, < 2.0.0) - google-apis-androidpublisher_v3 (~> 0.3) - google-apis-playcustomapp_v1 (~> 0.1) - google-cloud-env (>= 1.6.0, < 2.0.0) - google-cloud-storage (~> 1.31) - highline (~> 2.0) - http-cookie (~> 1.0.5) - json (< 3.0.0) - jwt (>= 2.1.0, < 3) - mini_magick (>= 4.9.4, < 5.0.0) - multipart-post (>= 2.0.0, < 3.0.0) - naturally (~> 2.2) - optparse (>= 0.1.1, < 1.0.0) - plist (>= 3.1.0, < 4.0.0) - rubyzip (>= 2.0.0, < 3.0.0) - security (= 0.1.5) - simctl (~> 1.6.3) - terminal-notifier (>= 2.0.0, < 3.0.0) - terminal-table (~> 3) - tty-screen (>= 0.6.3, < 1.0.0) - tty-spinner (>= 0.8.0, < 1.0.0) - word_wrap (~> 1.0.0) - xcodeproj (>= 1.13.0, < 2.0.0) - xcpretty (~> 0.3.0) - xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) - fastlane-plugin-pgyer (0.2.9) - gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.54.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-core (0.11.3) - addressable (~> 2.5, >= 2.5.1) - googleauth (>= 0.16.2, < 2.a) - httpclient (>= 2.8.1, < 3.a) - mini_mime (~> 1.0) - representable (~> 3.0) - retriable (>= 2.0, < 4.a) - rexml - google-apis-iamcredentials_v1 (0.17.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-playcustomapp_v1 (0.13.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-storage_v1 (0.29.0) - google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.6.1) - google-cloud-env (>= 1.0, < 3.a) - google-cloud-errors (~> 1.0) - google-cloud-env (1.6.0) - faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.3.1) - google-cloud-storage (1.45.0) - addressable (~> 2.8) - digest-crc (~> 0.4) - google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.29.0) - google-cloud-core (~> 1.6) - googleauth (>= 0.16.2, < 2.a) - mini_mime (~> 1.0) - googleauth (1.8.1) - faraday (>= 0.17.3, < 3.a) - jwt (>= 1.4, < 3.0) - multi_json (~> 1.11) - os (>= 0.9, < 2.0) - signet (>= 0.16, < 2.a) - highline (2.0.3) - http-cookie (1.0.7) - domain_name (~> 0.5) - httpclient (2.8.3) - jmespath (1.6.2) - json (2.7.2) - jwt (2.9.1) - base64 - mini_magick (4.13.2) - mini_mime (1.1.5) - multi_json (1.15.0) - multipart-post (2.4.1) - nanaimo (0.3.0) - naturally (2.2.1) - nkf (0.2.0) - optparse (0.5.0) - os (1.1.4) - plist (3.7.1) - public_suffix (5.1.1) - rake (13.2.1) - representable (3.2.0) - declarative (< 0.1.0) - trailblazer-option (>= 0.1.1, < 0.2.0) - uber (< 0.2.0) - retriable (3.1.2) - rexml (3.3.7) - rouge (2.0.7) - ruby2_keywords (0.0.5) - rubyzip (2.3.2) - security (0.1.5) - signet (0.18.0) - addressable (~> 2.8) - faraday (>= 0.17.5, < 3.a) - jwt (>= 1.5, < 3.0) - multi_json (~> 1.10) - simctl (1.6.10) - CFPropertyList - naturally - terminal-notifier (2.0.0) - terminal-table (3.0.2) - unicode-display_width (>= 1.1.1, < 3) - trailblazer-option (0.1.2) - tty-cursor (0.7.1) - tty-screen (0.8.2) - tty-spinner (0.9.3) - tty-cursor (~> 0.7) - uber (0.1.0) - unf (0.2.0) - unicode-display_width (2.6.0) - word_wrap (1.0.0) - xcodeproj (1.25.0) - CFPropertyList (>= 2.3.3, < 4.0) - atomos (~> 0.1.3) - claide (>= 1.0.2, < 2.0) - colored2 (~> 3.1) - nanaimo (~> 0.3.0) - rexml (>= 3.3.2, < 4.0) - xcpretty (0.3.0) - rouge (~> 2.0.7) - xcpretty-travis-formatter (1.0.1) - xcpretty (~> 0.2, >= 0.0.7) - -PLATFORMS - ruby - -DEPENDENCIES - fastlane - fastlane-plugin-pgyer - -BUNDLED WITH - 1.17.2 From cb48492f3850ad8fc2eabca29eea72e2cf8e6234 Mon Sep 17 00:00:00 2001 From: liyi Date: Sat, 17 May 2025 18:06:55 +0800 Subject: [PATCH 138/151] =?UTF-8?q?fix:=E6=81=A2=E5=A4=8D=E5=8E=9F?= =?UTF-8?q?=E6=9C=89ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 64896dab..a7726361 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -63,6 +63,8 @@ variables: - ls -li - export NEXT_VERSION="$(cat app_new.version)" # - flutter pub get + - export PATH="/opt/homebrew/bin:$PATH" + - eval "$(rbenv init -)" - bundle install --gemfile android/Gemfile --quiet cache: paths: From 0b5fb23eec99b4b909c5a33605c1a8e2aa1f1317 Mon Sep 17 00:00:00 2001 From: liyi Date: Sat, 17 May 2025 18:12:39 +0800 Subject: [PATCH 139/151] =?UTF-8?q?fix:=E6=81=A2=E5=A4=8D=E5=8E=9F?= =?UTF-8?q?=E6=9C=89ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a7726361..a584feb6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -65,6 +65,7 @@ variables: # - flutter pub get - export PATH="/opt/homebrew/bin:$PATH" - eval "$(rbenv init -)" + - bundle config mirror.https://rubygems.org https://gems.ruby-china.com - bundle install --gemfile android/Gemfile --quiet cache: paths: @@ -76,6 +77,9 @@ variables: - ls -li - export NEXT_VERSION="$(cat app_new.version)" # - flutter pub get + - export PATH="/opt/homebrew/bin:$PATH" + - eval "$(rbenv init -)" + - bundle config mirror.https://rubygems.org https://gems.ruby-china.com - bundle install --gemfile ios/Gemfile --quiet cache: paths: From 6a3992b8e2c6dcba479632181ed5881ab48b1230 Mon Sep 17 00:00:00 2001 From: liyi Date: Mon, 19 May 2025 09:23:21 +0800 Subject: [PATCH 140/151] =?UTF-8?q?fix:=E6=81=A2=E5=A4=8D=E5=8E=9F?= =?UTF-8?q?=E6=9C=89ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 85c78d24..45dfc905 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # 星锁APP -fix:测试ci + 星云项目组旗下的智能锁应用,其中锁相关数据接入星云平台,业务数据接入星锁自有后台。 基于Flutter技术架构,支持Android和iOS平台。 From 092a0d00b791f0304cba2fbe3b5666c91773b731 Mon Sep 17 00:00:00 2001 From: liyi Date: Mon, 19 May 2025 10:13:30 +0800 Subject: [PATCH 141/151] =?UTF-8?q?fix:=E6=81=A2=E5=A4=8D=E5=8E=9F?= =?UTF-8?q?=E6=9C=89ci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../burglarAlarm/burglarAlarm_logic.dart | 69 +++++++++++-------- .../remoteUnlocking_logic.dart | 9 +-- 2 files changed, 44 insertions(+), 34 deletions(-) diff --git a/lib/main/lockDetail/lockSet/burglarAlarm/burglarAlarm_logic.dart b/lib/main/lockDetail/lockSet/burglarAlarm/burglarAlarm_logic.dart index 74e2c236..95830a3b 100755 --- a/lib/main/lockDetail/lockSet/burglarAlarm/burglarAlarm_logic.dart +++ b/lib/main/lockDetail/lockSet/burglarAlarm/burglarAlarm_logic.dart @@ -1,4 +1,3 @@ - import 'dart:async'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; @@ -17,31 +16,36 @@ import '../../../../tools/eventBusEventManage.dart'; import '../../../../tools/storage.dart'; import 'burglarAlarm_state.dart'; -class BurglarAlarmLogic extends BaseGetXController{ +class BurglarAlarmLogic extends BaseGetXController { BurglarAlarmState state = BurglarAlarmState(); // 配置锁的常开模式设置 -> 防撬报警 - Future _setLockSetGeneralSetting() async{ + Future _setLockSetGeneralSetting() async { final LoginEntity entity = await ApiRepository.to.setBurglarAlarmData( lockId: state.lockSetInfoData.value.lockId!, - antiPrySwitch:state.burglarAlarmEnable.value == 1 ? 0 : 1, // 1-开启、2-关闭; + antiPrySwitch: state.burglarAlarmEnable.value == 1 ? 0 : 1, // 1-开启、2-关闭; ); - if(entity.errorCode!.codeIsSuccessful){ - // eventBus.fire(RefreshLockListInfoDataEvent()); + if (entity.errorCode!.codeIsSuccessful) { + eventBus.fire(RefreshLockListInfoDataEvent()); - state.burglarAlarmEnable.value = state.burglarAlarmEnable.value == 1 ? 0 : 1; - state.lockSetInfoData.value.lockSettingInfo!.antiPrySwitch = state.burglarAlarmEnable.value; - showToast('操作成功'.tr, something: (){ - eventBus.fire(PassCurrentLockInformationEvent(state.lockSetInfoData.value)); + state.burglarAlarmEnable.value = + state.burglarAlarmEnable.value == 1 ? 0 : 1; + state.lockSetInfoData.value.lockSettingInfo!.antiPrySwitch = + state.burglarAlarmEnable.value; + showToast('操作成功'.tr, something: () { + eventBus + .fire(PassCurrentLockInformationEvent(state.lockSetInfoData.value)); }); } } // 获取解析后的数据 late StreamSubscription _replySubscription; + void _initReplySubscription() { - _replySubscription = EventBusManager().eventBus!.on().listen((Reply reply) { - if(reply is SetSupportFunctionsNoParametersReply) { + _replySubscription = + EventBusManager().eventBus!.on().listen((Reply reply) { + if (reply is SetSupportFunctionsNoParametersReply) { _replySetSupportFunctionsWithParameters(reply); } @@ -71,7 +75,7 @@ class BurglarAlarmLogic extends BaseGetXController{ // 设置自动落锁数据解析 Future _replySetSupportFunctionsWithParameters(Reply reply) async { final int status = reply.data[2]; - switch(status){ + switch (status) { case 0x00: //成功 state.sureBtnState.value = 0; @@ -91,41 +95,47 @@ class BurglarAlarmLogic extends BaseGetXController{ // 设置支持功能(带参数) Future sendBurglarAlarm() async { - if(state.sureBtnState.value == 1){ + if (state.sureBtnState.value == 1) { return; } state.sureBtnState.value = 1; EasyLoading.show(); - showBlueConnetctToastTimer(action: (){ + showBlueConnetctToastTimer(action: () { dismissEasyLoading(); state.sureBtnState.value = 0; }); - BlueManage().blueSendData(BlueManage().connectDeviceName, (BluetoothConnectionState connectionState) async { + BlueManage().blueSendData(BlueManage().connectDeviceName, + (BluetoothConnectionState connectionState) async { if (connectionState == BluetoothConnectionState.connected) { - final List? privateKey = await Storage.getStringList(saveBluePrivateKey); - final List getPrivateKeyList = changeStringListToIntList(privateKey!); + final List? privateKey = + await Storage.getStringList(saveBluePrivateKey); + final List getPrivateKeyList = + changeStringListToIntList(privateKey!); final List? token = await Storage.getStringList(saveBlueToken); final List getTokenList = changeStringListToIntList(token!); - final List? publicKey = await Storage.getStringList(saveBluePublicKey); - final List getPublicKeyList = changeStringListToIntList(publicKey!); + final List? publicKey = + await Storage.getStringList(saveBluePublicKey); + final List getPublicKeyList = + changeStringListToIntList(publicKey!); IoSenderManage.setSupportFunctionsNoParametersCommand( - keyID: state.lockSetInfoData.value.lockBasicInfo!.keyId.toString(), - userID: await Storage.getUid(), - featureBit: 30, - featureEnable: state.burglarAlarmEnable.value == 1 ? 0 : 1, - token: getTokenList, - needAuthor: 1, - publicKey: getPublicKeyList, - privateKey: getPrivateKeyList); + keyID: state.lockSetInfoData.value.lockBasicInfo!.keyId.toString(), + userID: await Storage.getUid(), + featureBit: 30, + featureEnable: state.burglarAlarmEnable.value == 1 ? 0 : 1, + token: getTokenList, + needAuthor: 1, + publicKey: getPublicKeyList, + privateKey: getPrivateKeyList, + ); } else if (connectionState == BluetoothConnectionState.disconnected) { dismissEasyLoading(); cancelBlueConnetctToastTimer(); state.sureBtnState.value = 0; - if(state.ifCurrentScreen.value == true){ + if (state.ifCurrentScreen.value == true) { showBlueConnetctToast(); } } @@ -152,5 +162,4 @@ class BurglarAlarmLogic extends BaseGetXController{ _replySubscription.cancel(); } - } diff --git a/lib/main/lockDetail/lockSet/remoteUnlocking/remoteUnlocking_logic.dart b/lib/main/lockDetail/lockSet/remoteUnlocking/remoteUnlocking_logic.dart index d5cc7d9d..3d62a5c1 100755 --- a/lib/main/lockDetail/lockSet/remoteUnlocking/remoteUnlocking_logic.dart +++ b/lib/main/lockDetail/lockSet/remoteUnlocking/remoteUnlocking_logic.dart @@ -21,9 +21,10 @@ class RemoteUnlockingLogic extends BaseGetXController { RemoteUnlockingState state = RemoteUnlockingState(); void remoteUnlockingOpenOrClose() async { - final LoginEntity entity = await ApiRepository.to.remoteUnlockingOpenOrClose( - lockId: state.lockSetInfoData.value.lockId!, - remoteUnlock: state.remoteEnable.value == 1 ? 0 : 1); + final LoginEntity entity = await ApiRepository.to + .remoteUnlockingOpenOrClose( + lockId: state.lockSetInfoData.value.lockId!, + remoteUnlock: state.remoteEnable.value == 1 ? 0 : 1); if (entity.errorCode!.codeIsSuccessful) { showToast('操作成功'.tr, something: () { eventBus.fire(RefreshLockListInfoDataEvent()); @@ -32,7 +33,6 @@ class RemoteUnlockingLogic extends BaseGetXController { state.remoteEnable.value; eventBus .fire(PassCurrentLockInformationEvent(state.lockSetInfoData.value)); - eventBus.fire(RefreshLockListInfoDataEvent()); eventBus.fire(LockSetChangeSetRefreshLockDetailWithType( 5, state.lockSetInfoData.value.lockSettingInfo!.remoteUnlock! @@ -44,6 +44,7 @@ class RemoteUnlockingLogic extends BaseGetXController { // 获取解析后的数据 late StreamSubscription _replySubscription; + void _initReplySubscription() { _replySubscription = EventBusManager().eventBus!.on().listen((reply) { From 35f7dd38e324f6974849678fa4c81107b21a7c2e Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 30 May 2025 10:54:37 +0800 Subject: [PATCH 142/151] =?UTF-8?q?fix:=E5=90=8C=E6=AD=A5=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=90=8E=E7=9A=84ci=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/Gemfile | 2 +- ios/Gemfile | 1 + ios/Gemfile.lock | 1 + ios/fastlane/Fastfile | 6 ++++-- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/android/Gemfile b/android/Gemfile index cdd3a6b3..edb07877 100644 --- a/android/Gemfile +++ b/android/Gemfile @@ -1,6 +1,6 @@ source "https://rubygems.org" gem "fastlane" - +gem 'nkf', '0.2.0' plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/ios/Gemfile b/ios/Gemfile index 21cb5dfc..96415b71 100644 --- a/ios/Gemfile +++ b/ios/Gemfile @@ -3,5 +3,6 @@ source "https://rubygems.org" gem "fastlane" gem 'cocoapods', '1.14.3' gem 'public_suffix', '~> 4.0' +gem 'nkf', '0.2.0' plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index 8882903d..e8a3ba4d 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -282,6 +282,7 @@ DEPENDENCIES cocoapods (= 1.14.3) fastlane fastlane-plugin-pgyer + nkf (= 0.2.0) public_suffix (~> 4.0) BUNDLED WITH diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 3591770d..895ba92e 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -89,7 +89,8 @@ platform :ios do sh("flutter","pub","get") end Dir.chdir ".." do - sh("bundle", "exec" ,"pod", "install") + #sh("bundle", "exec" ,"pod", "install") + sh("pod", "install") end Dir.chdir "../.." do sh("flutter", "build", "ios", "--no-tree-shake-icons", "--no-codesign", "--release", "--flavor", "#{flavor}", "-t", "lib/main_#{flavor}_#{env}.dart", "--build-number=#{build_number}", "--build-name=#{build_version}") @@ -142,7 +143,8 @@ platform :ios do sh("flutter","pub","get") end Dir.chdir ".." do - sh("bundle", "exec" ,"pod", "install") + #sh("bundle", "exec" ,"pod", "install") + sh("pod", "install") end Dir.chdir "../.." do sh("flutter", "build", "ios", "--no-tree-shake-icons", "--no-codesign", "--release", "--flavor", "#{flavor}", "-t", "lib/main_#{flavor}_lite.dart", "--build-number=#{build_number}", "--build-name=#{build_version}") From 5af587466c477bed73ad55a4f614b3384e604a1b Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 30 May 2025 11:34:03 +0800 Subject: [PATCH 143/151] =?UTF-8?q?fix:ci=E6=B5=81=E7=A8=8B=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E4=B8=BAsky=E5=88=86=E6=94=AF=E6=9E=84=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 18 +++++----- android/build.sh | 9 ++--- ios/build.sh | 9 ++--- notify.sh | 4 +-- tag_generator.sh | 93 +++++++++++++++++++++++++++++++++++------------- 5 files changed, 89 insertions(+), 44 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a584feb6..960da81d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,10 +16,10 @@ variables: - macos - flutter rules: - - if: $CI_COMMIT_BRANCH == "develop" - - if: $CI_COMMIT_BRANCH == "release" + - if: $CI_COMMIT_BRANCH == "develop_sky" + - if: $CI_COMMIT_BRANCH == "release_sky" - if: $CI_COMMIT_BRANCH =~ /feat_[a-zA-Z]+/ - - if: $CI_COMMIT_BRANCH == "canary_release" + - if: $CI_COMMIT_BRANCH == "canary_release_sky" - if: $CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]+\.[0-9]+)?$/ .notify_rule: @@ -27,8 +27,8 @@ variables: - macos - flutter rules: - - if: $CI_COMMIT_BRANCH == "develop" - - if: $CI_COMMIT_BRANCH == "release" + - if: $CI_COMMIT_BRANCH == "develop_sky" + - if: $CI_COMMIT_BRANCH == "release_sky" - if: $CI_COMMIT_BRANCH =~ /feat_[a-zA-Z]+/ .generate_tag_rule: @@ -36,16 +36,16 @@ variables: - macos - flutter rules: - - if: $CI_COMMIT_BRANCH == "master" + - if: $CI_COMMIT_BRANCH == "master_sky" .generate_next_version_rule: tags: - macos - flutter rules: - - if: $CI_COMMIT_BRANCH == "develop" - - if: $CI_COMMIT_BRANCH == "release" - - if: $CI_COMMIT_BRANCH == "canary_release" + - if: $CI_COMMIT_BRANCH == "develop_sky" + - if: $CI_COMMIT_BRANCH == "release_sky" + - if: $CI_COMMIT_BRANCH == "canary_release_sky" - if: $CI_COMMIT_BRANCH =~ /feat_[a-zA-Z]+/ .print_env: diff --git a/android/build.sh b/android/build.sh index 4b598959..40a15413 100755 --- a/android/build.sh +++ b/android/build.sh @@ -8,8 +8,9 @@ export ENV_BUILD_WORKSPACE=${CI_PROJECT_DIR} echo "GITLAB_WORKSPACE: ${CI_PROJECT_DIR}" cd ${CI_PROJECT_DIR}/android echo "ENV_BUILD_TAG:${ENV_BUILD_TAG},ENV_BUILD_BRANCH:${ENV_BUILD_BRANCH}" -regex='^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]+\.[0-9]+)?$' -if [[ "${ENV_BUILD_BRANCH}" == "canary_release" ]]; then +# 只支持 v1.2.3_sky 这种tag格式 +regex='^v[0-9]+\.[0-9]+\.[0-9]+_sky$' +if [[ "${ENV_BUILD_BRANCH}" == "canary_release_sky" ]]; then echo "===build canary_release: ${NEXT_VERSION}" export ENV_BUILD_TAG=${NEXT_VERSION} bundle exec fastlane release_apk flavor:xhj --verbose @@ -20,11 +21,11 @@ elif [[ $ENV_BUILD_TAG =~ $regex ]]; then bundle exec fastlane release_apk flavor:sky --verbose bundle exec fastlane release_bundle flavor:xhj_bundle --verbose bundle exec fastlane release_bundle flavor:sky --verbose -elif [[ "${ENV_BUILD_BRANCH}" == "develop" ]]; then +elif [[ "${ENV_BUILD_BRANCH}" == "develop_sky" ]]; then echo "===build dev===${NEXT_VERSION}" bundle exec fastlane beta flavor:xhj env:dev --verbose bundle exec fastlane beta flavor:sky env:dev --verbose -elif [[ "${ENV_BUILD_BRANCH}" == "release" ]] || [[ "${ENV_BUILD_BRANCH}" == "feat_devops" ]] ; then +elif [[ "${ENV_BUILD_BRANCH}" == "release_sky" || "${ENV_BUILD_BRANCH}" == "feat_devops_sky" ]] ; then echo "===build pre===${NEXT_VERSION}" bundle exec fastlane beta flavor:xhj env:pre --verbose bundle exec fastlane beta flavor:sky env:pre --verbose diff --git a/ios/build.sh b/ios/build.sh index a8a977bf..2c46b1f1 100755 --- a/ios/build.sh +++ b/ios/build.sh @@ -9,8 +9,9 @@ echo "GITLAB_WORKSPACE: ${CI_PROJECT_DIR}" cd ${CI_PROJECT_DIR}/ios #bundle exec pod install echo "ENV_BUILD_TAG:${ENV_BUILD_TAG},ENV_BUILD_BRANCH:${ENV_BUILD_BRANCH}" -regex='^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z]+\.[0-9]+)?$' -if [[ "${ENV_BUILD_BRANCH}" == "canary_release" ]]; then +# 只支持 v1.2.3_sky 这种tag格式 +regex='^v[0-9]+\.[0-9]+\.[0-9]+_sky$' +if [[ "${ENV_BUILD_BRANCH}" == "canary_release_sky" ]]; then echo "===build canary_release: ${NEXT_VERSION}" export ENV_BUILD_TAG=${NEXT_VERSION} bundle exec fastlane release_ipa flavor:xhj --verbose @@ -19,11 +20,11 @@ elif [[ $ENV_BUILD_TAG =~ $regex ]]; then echo "===build release===$ENV_BUILD_TAG" bundle exec fastlane release_ipa flavor:xhj --verbose bundle exec fastlane release_ipa flavor:sky --verbose -elif [[ "${ENV_BUILD_BRANCH}" == "develop" ]]; then +elif [[ "${ENV_BUILD_BRANCH}" == "develop_sky" ]]; then echo "===build dev===${NEXT_VERSION}" bundle exec fastlane beta flavor:xhj env:Dev --verbose bundle exec fastlane beta flavor:sky env:Dev --verbose -elif [[ "${ENV_BUILD_BRANCH}" == "release" ]] || [[ "${ENV_BUILD_BRANCH}" == "feat_devops" ]] ; then +elif [[ "${ENV_BUILD_BRANCH}" == "release_sky" || "${ENV_BUILD_BRANCH}" == "feat_devops_sky" ]] ; then echo "===build pre===${NEXT_VERSION}" bundle exec fastlane beta flavor:xhj env:Pre --verbose bundle exec fastlane beta flavor:sky env:Pre --verbose diff --git a/notify.sh b/notify.sh index b881f57f..0a5ecf90 100755 --- a/notify.sh +++ b/notify.sh @@ -2,13 +2,13 @@ set -e APP_PRODUCT_NAME=$APP_PRODUCT_NAME BUILD_STATUS=$1 -if [[ "${CI_COMMIT_BRANCH}" == "release" ]] ; then +if [[ "${CI_COMMIT_BRANCH}" == "release_sky" ]] ; then WECAHT_WEBHOOK_URL=$PRE_QYWECAHT_WEBHOOK_URL SKY_IOS_DOWNLOAD_URL=$PRE_SKY_IOS_DOWNLOAD_URL SKY_ANDROID_DOWNLOAD_URL=$PRE_SKY_ANDROID_DOWNLOAD_URL XHJ_IOS_DOWNLOAD_URL=$PRE_XHJ_IOS_DOWNLOAD_URL XHJ_ANDROID_DOWNLOAD_URL=$PRE_XHJ_ANDROID_DOWNLOAD_URL -elif [[ "${CI_COMMIT_BRANCH}" == "develop" ]] || [[ "${CI_COMMIT_BRANCH}" == "feat_devops" ]]; then +elif [[ "${CI_COMMIT_BRANCH}" == "develop_sky" ]] || [[ "${CI_COMMIT_BRANCH}" == "feat_devops_sky" ]]; then WECAHT_WEBHOOK_URL=$DEV_QYWECAHT_WEBHOOK_URL SKY_IOS_DOWNLOAD_URL=$DEV_SKY_IOS_DOWNLOAD_URL SKY_ANDROID_DOWNLOAD_URL=$DEV_SKY_ANDROID_DOWNLOAD_URL diff --git a/tag_generator.sh b/tag_generator.sh index 27e39182..5fd79429 100755 --- a/tag_generator.sh +++ b/tag_generator.sh @@ -1,63 +1,106 @@ #!/bin/bash -# 读取环境变量 -URL=$CI_API_V4_URL -TOKEN=$GITLAB_ACCESS_TOKEN -PROJECT_ID=$CI_PROJECT_ID -next_tag="" -newest_tag="" +# ======================== +# tag_generator.sh +# 用于自动识别并递增以 _sky 结尾的 tag(如 v1.5.556_sky),并生成下一个 tag。 +# 递增规则:feat: 提交递增 minor,fix: 或其他提交递增 patch。 +# ======================== + +# 读取环境变量(由 CI/CD 系统注入) +URL=$CI_API_V4_URL # GitLab API v4 地址 +TOKEN=$GITLAB_ACCESS_TOKEN # GitLab 访问 Token +PROJECT_ID=$CI_PROJECT_ID # 当前项目 ID +next_tag="" # 新的 tag 变量 +newest_tag="" # 最新 tag 变量 + echo "PRIVATE-TOKEN: $TOKEN $URL/projects/$PROJECT_ID/repository/tags" +# 获取项目所有 tag 的 json 列表 +# 需要 jq 工具解析 json +# tags_json 是所有 tag 的原始 json 数据 tags_json=$(curl -H "Content-Type: application/json" -H "PRIVATE-TOKEN: $TOKEN" "$URL/projects/$PROJECT_ID/repository/tags") -#echo "tags_json:$tags_json\n" +# 提取所有 tag 名称,按时间倒序排列(最新的在前) tags=$(echo "$tags_json" | jq -r '.[].name') -tags_length=$(echo "$tags_json" | jq -r 'length') -if [ "$tags_length" -lt 1 ]; then - next_tag="v1.0.0" + +# 只保留以 _sky 结尾的 tag(即只处理 v1.2.3_sky 这种格式的 tag) +sky_tags=$(echo "$tags" | grep '_sky$') +# 取最新的 _sky tag(即第一个) +newest_sky_tag=$(echo "$sky_tags" | head -n 1) + +# 如果没有 _sky 结尾的 tag,则从最新的 tag 提取版本号,生成 vX.Y.Z_sky +if [ -z "$newest_sky_tag" ]; then + # 取最新的 tag(不管是否带_sky) + latest_tag=$(echo "$tags" | head -n 1) + if [ -n "$latest_tag" ]; then + # 提取版本号部分(去掉前缀v和后缀_sky等) + version_part=${latest_tag#v} # 去掉v + version_part=${version_part%_sky} # 去掉_sky(如果有) + IFS='.' read -r major minor patch <<< "$version_part" + next_tag="v$major.$minor.$patch_sky" + else + next_tag="v1.0.0_sky" + fi else - newest_tag=$(echo "$tags" | head -n 1) - IFS='.' read -r major minor patch <<< "$newest_tag" - major="${major#v}" - compare_json="" + # 解析版本号部分(去掉 _sky 后缀和 v 前缀) + # 例如 v1.5.556_sky -> 1.5.556 + version_part=${newest_sky_tag%_sky} # 去掉 _sky 后缀 + version_part=${version_part#v} # 去掉 v 前缀 + IFS='.' read -r major minor patch <<< "$version_part" # 拆分出主、次、修订号 + compare_json="" # 用于存储 commit 对比结果 + # 判断命令参数,决定对比范围 if [[ "$1" == "generate_tag" ]];then - echo "generate_tag:$newest_tag-to-master\n" - compare_json=$(curl -s --header "PRIVATE-TOKEN: $TOKEN" "$URL/projects/$PROJECT_ID/repository/compare?from=$newest_tag&to=master") + # 生成 tag 时,对比最新 _sky tag 和 master 之间的提交 + echo "generate_tag:$newest_sky_tag-to-master\n" + compare_json=$(curl -s --header "PRIVATE-TOKEN: $TOKEN" "$URL/projects/$PROJECT_ID/repository/compare?from=$newest_sky_tag&to=master") elif [[ "$1" == "generate_version" ]]; then - echo "generate_version:master-to-$CI_COMMIT_BRANCH\n" + # 生成版本号时,对比 master 和当前分支之间的提交 + echo "generate_version:master-to-$CI_COMMIT_BRANCH\n" compare_json=$(curl -s --header "PRIVATE-TOKEN: $TOKEN" "$URL/projects/$PROJECT_ID/repository/compare?from=master&to=$CI_COMMIT_BRANCH") fi echo "compare_json:$compare_json\n" - new_patch=$patch - new_minor=$minor + new_patch=$patch # 新的 patch 号 + new_minor=$minor # 新的 minor 号 + # 遍历所有 commit,根据提交信息递增版本号 while IFS= read -r commit_json; do - # 使用 jq 解析每一行的 JSON 对象 + # 解析每个 commit 的 id 和 message commit_id=$(echo "$commit_json" | jq -r '.id') commit_message=$(echo "$commit_json" | jq -r '.message') echo "----$commit_message" + # 如果有 feat: 类型提交,minor 递增(只递增一次) if [[ "$commit_message" =~ ("feat:"*) ]] && [[ $new_minor == $minor ]]; then ((new_minor++)) -# new_patch=0 -# break + # 如果有 fix: 类型提交,patch 递增 elif [[ "$commit_message" =~ ("fix:"*) ]]; then ((new_patch++)) + # 其他类型提交(非 Merge/Revert),patch 递增 elif [[ ! "$commit_message" =~ ("Merge"* | "Revert"*) ]]; then ((new_patch++)) fi done < <(echo "$compare_json" | jq -c '.commits[] | {id: .id, message: .message}') - next_tag="v$major.$new_minor.$new_patch" + # 组装新的 tag,格式为 v.._sky + next_tag="v$major.$new_minor.$new_patch_sky" fi -echo "New Tag:$newest_tag;New version: $next_tag;command: $1" + +echo "New Tag:$newest_sky_tag;New version: $next_tag;command: $1" + +# 如果是 generate_tag 命令,且新 tag 和最新 tag 一致,则跳过生成 if [[ "$1" == "generate_tag" ]];then - if [ "$next_tag" == "$newest_tag" ]; then + if [ "$next_tag" == "$newest_sky_tag" ]; then echo "no change from master,skip to generate tag" exit 0 fi + # 配置 git 用户名(可根据需要补充邮箱等) git config user.name + # 创建并推送新 tag git tag $next_tag git push -u origin $next_tag echo "generate tag: $next_tag" elif [[ "$1" == "generate_version" ]]; then + # 如果是 generate_version 命令,仅导出新版本号到环境变量 export NEXT_VERSION="$next_tag" echo "generate version: $NEXT_VERSION" fi +# 无论哪种情况,都把新 tag 写入 app_new.version 文件 +# 供后续流程使用 + echo "$next_tag" > app_new.version From c26f241b2b2dacb44081bd6eb7dd95c0ac03910c Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 30 May 2025 11:36:17 +0800 Subject: [PATCH 144/151] =?UTF-8?q?fix:=E6=B5=8B=E8=AF=95skyci=E6=B5=81?= =?UTF-8?q?=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 45dfc905..05d4216d 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # 星锁APP - +测试skyci流程 星云项目组旗下的智能锁应用,其中锁相关数据接入星云平台,业务数据接入星锁自有后台。 基于Flutter技术架构,支持Android和iOS平台。 From dac34b8824ca465fe6cac9d132f8cbc6e8c108e6 Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 30 May 2025 11:49:54 +0800 Subject: [PATCH 145/151] =?UTF-8?q?fix:=E6=B5=8B=E8=AF=95skyci=E6=B5=81?= =?UTF-8?q?=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 05d4216d..45dfc905 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # 星锁APP -测试skyci流程 + 星云项目组旗下的智能锁应用,其中锁相关数据接入星云平台,业务数据接入星锁自有后台。 基于Flutter技术架构,支持Android和iOS平台。 From 9af93869a83cb092085a96acdf1fe23a03d54d56 Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 30 May 2025 12:19:58 +0800 Subject: [PATCH 146/151] =?UTF-8?q?fix:=E6=B5=8B=E8=AF=95skyci=E6=B5=81?= =?UTF-8?q?=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 960da81d..87ad41db 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -65,6 +65,7 @@ variables: # - flutter pub get - export PATH="/opt/homebrew/bin:$PATH" - eval "$(rbenv init -)" + - rbenv --version - bundle config mirror.https://rubygems.org https://gems.ruby-china.com - bundle install --gemfile android/Gemfile --quiet cache: @@ -79,6 +80,7 @@ variables: # - flutter pub get - export PATH="/opt/homebrew/bin:$PATH" - eval "$(rbenv init -)" + - rbenv --version - bundle config mirror.https://rubygems.org https://gems.ruby-china.com - bundle install --gemfile ios/Gemfile --quiet cache: From 285211804b8ab27e30d6e62a2887d063b7cb8167 Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 30 May 2025 12:29:36 +0800 Subject: [PATCH 147/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4rbenv=20global?= =?UTF-8?q?=E7=89=88=E6=9C=AC=EF=BC=8C=E4=BF=AE=E5=A4=8Dbuild=5Fandroid?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 87ad41db..767f6b7d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -64,8 +64,11 @@ variables: - export NEXT_VERSION="$(cat app_new.version)" # - flutter pub get - export PATH="/opt/homebrew/bin:$PATH" + - rbenv install 2.7.8 --skip-existing + - rbenv global 2.7.8 - eval "$(rbenv init -)" - rbenv --version + - ruby -v - bundle config mirror.https://rubygems.org https://gems.ruby-china.com - bundle install --gemfile android/Gemfile --quiet cache: From 0e399c5831e7d028267dfdeecf24cc8075c6713e Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 30 May 2025 12:34:35 +0800 Subject: [PATCH 148/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4rbenv=20global?= =?UTF-8?q?=E7=89=88=E6=9C=AC=EF=BC=8C=E4=BF=AE=E5=A4=8Dbuild=5Fandroid?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 767f6b7d..1a2a2d3d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -64,6 +64,7 @@ variables: - export NEXT_VERSION="$(cat app_new.version)" # - flutter pub get - export PATH="/opt/homebrew/bin:$PATH" + - export RUBY_BUILD_MIRROR_URL=https://cache.ruby-china.com/pub/ruby - rbenv install 2.7.8 --skip-existing - rbenv global 2.7.8 - eval "$(rbenv init -)" From a94e80e6708e0a9ba6703e92387898084cb8ad85 Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 30 May 2025 13:48:19 +0800 Subject: [PATCH 149/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4rbenv=20global?= =?UTF-8?q?=E7=89=88=E6=9C=AC=EF=BC=8C=E4=BF=AE=E5=A4=8Dbuild=5Fandroid?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 2 +- pubspec.lock | 80 +++++--------------------------------------------- 2 files changed, 9 insertions(+), 73 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 1a2a2d3d..e0c89d10 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -64,7 +64,6 @@ variables: - export NEXT_VERSION="$(cat app_new.version)" # - flutter pub get - export PATH="/opt/homebrew/bin:$PATH" - - export RUBY_BUILD_MIRROR_URL=https://cache.ruby-china.com/pub/ruby - rbenv install 2.7.8 --skip-existing - rbenv global 2.7.8 - eval "$(rbenv init -)" @@ -85,6 +84,7 @@ variables: - export PATH="/opt/homebrew/bin:$PATH" - eval "$(rbenv init -)" - rbenv --version + - ruby -v - bundle config mirror.https://rubygems.org https://gems.ruby-china.com - bundle install --gemfile ios/Gemfile --quiet cache: diff --git a/pubspec.lock b/pubspec.lock index b1ead27d..b099b594 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -24,30 +24,6 @@ packages: relative: true source: path version: "0.0.1" - amap_flutter_base: - dependency: transitive - description: - name: amap_flutter_base - sha256: "9ef2439b8de7100cdd1b4357701b8ca8c059c0f2d9d0257b81750bbf0c6f53bb" - url: "https://pub.dev" - source: hosted - version: "3.0.0" - amap_flutter_location: - dependency: "direct main" - description: - name: amap_flutter_location - sha256: f35ff00e196d579608e0bc28ccbc1f6f53787644702f947de941f775769cc701 - url: "https://pub.dev" - source: hosted - version: "3.0.0" - amap_flutter_map: - dependency: "direct main" - description: - name: amap_flutter_map - sha256: "9cebb0b2f5fc7d3ae0427e99c41edc883e8f5459f6a28bc850f0f9e16918cf2f" - url: "https://pub.dev" - source: hosted - version: "3.0.0" app_settings: dependency: "direct main" description: @@ -573,6 +549,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_app_badger: + dependency: "direct main" + description: + name: flutter_app_badger + sha256: "64d4a279bab862ed28850431b9b446b9820aaae0bf363322d51077419f930fa8" + url: "https://pub.dev" + source: hosted + version: "1.5.0" flutter_blue_plus: dependency: "direct main" description: @@ -788,30 +772,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" - google_maps: - dependency: transitive - description: - name: google_maps - sha256: "47eef3836b49bb030d5cb3afc60b8451408bf34cf753e571b645d6529eb4251a" - url: "https://pub.dev" - source: hosted - version: "7.1.0" - google_maps_flutter: - dependency: "direct main" - description: - name: google_maps_flutter - sha256: c1972cbad779bc5346c49045f26ae45550a0958b1cbca5b524dd3c8954995d28 - url: "https://pub.dev" - source: hosted - version: "2.6.1" - google_maps_flutter_android: - dependency: transitive - description: - name: google_maps_flutter_android - sha256: "0bcadb80eba39afda77dede89a6caafd3b68f2786b90491eceea4a01c3db181c" - url: "https://pub.dev" - source: hosted - version: "2.8.0" google_maps_flutter_ios: dependency: "direct overridden" description: @@ -828,14 +788,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.9.5" - google_maps_flutter_web: - dependency: transitive - description: - name: google_maps_flutter_web - sha256: f3155c12119d8a5c2732fdf39ceb5cc095bc662059a03b4ea23294ecebe1d199 - url: "https://pub.dev" - source: hosted - version: "0.5.8" html: dependency: transitive description: @@ -973,14 +925,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.7" - js_wrapping: - dependency: transitive - description: - name: js_wrapping - sha256: e385980f7c76a8c1c9a560dfb623b890975841542471eade630b2871d243851c - url: "https://pub.dev" - source: hosted - version: "0.7.4" json_annotation: dependency: transitive description: @@ -1381,14 +1325,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.27.7" - sanitize_html: - dependency: transitive - description: - name: sanitize_html - sha256: "12669c4a913688a26555323fb9cec373d8f9fbe091f2d01c40c723b33caa8989" - url: "https://pub.dev" - source: hosted - version: "2.1.0" scrollable_positioned_list: dependency: transitive description: From 2b29e510cea6b71c900738ec506c5a5d394e34c2 Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 30 May 2025 13:50:48 +0800 Subject: [PATCH 150/151] =?UTF-8?q?fix:=E8=B0=83=E6=95=B4rbenv=20global?= =?UTF-8?q?=E7=89=88=E6=9C=AC=EF=BC=8C=E4=BF=AE=E5=A4=8Dbuild=5Fandroid?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitlab-ci.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e0c89d10..960da81d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -64,11 +64,7 @@ variables: - export NEXT_VERSION="$(cat app_new.version)" # - flutter pub get - export PATH="/opt/homebrew/bin:$PATH" - - rbenv install 2.7.8 --skip-existing - - rbenv global 2.7.8 - eval "$(rbenv init -)" - - rbenv --version - - ruby -v - bundle config mirror.https://rubygems.org https://gems.ruby-china.com - bundle install --gemfile android/Gemfile --quiet cache: @@ -83,8 +79,6 @@ variables: # - flutter pub get - export PATH="/opt/homebrew/bin:$PATH" - eval "$(rbenv init -)" - - rbenv --version - - ruby -v - bundle config mirror.https://rubygems.org https://gems.ruby-china.com - bundle install --gemfile ios/Gemfile --quiet cache: From 34eabe7eeeb52862d7f4684376e584b3631b128d Mon Sep 17 00:00:00 2001 From: liyi Date: Fri, 30 May 2025 13:57:18 +0800 Subject: [PATCH 151/151] =?UTF-8?q?fix:=E6=B5=8B=E8=AF=95=E5=BC=80?= =?UTF-8?q?=E5=8F=91ci=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 45dfc905..d6ee8c02 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # 星锁APP - +测试ci 星云项目组旗下的智能锁应用,其中锁相关数据接入星云平台,业务数据接入星锁自有后台。 基于Flutter技术架构,支持Android和iOS平台。