diff --git a/lib/network/api_provider.dart b/lib/network/api_provider.dart index 6366aeee..e31b9522 100755 --- a/lib/network/api_provider.dart +++ b/lib/network/api_provider.dart @@ -2745,7 +2745,9 @@ class ApiProvider extends BaseProvider { jsonEncode({ 'deviceType': deviceType, 'deviceMac': deviceMac, - })); + }), + isShowNetworkErrorMsg: false, + isUnShowLoading: true); } extension ExtensionString on String { diff --git a/lib/network/api_repository.dart b/lib/network/api_repository.dart index 39c8e9e7..c2143360 100755 --- a/lib/network/api_repository.dart +++ b/lib/network/api_repository.dart @@ -2739,4 +2739,7 @@ class ApiRepository { ); return DeviceNetwork.fromJson(res.body); } + + + } diff --git a/lib/talk/startChart/handle/impl/udp_ble_passthrough_handler.dart b/lib/talk/startChart/handle/impl/udp_ble_passthrough_handler.dart index b582f421..10be6d6b 100644 --- a/lib/talk/startChart/handle/impl/udp_ble_passthrough_handler.dart +++ b/lib/talk/startChart/handle/impl/udp_ble_passthrough_handler.dart @@ -42,12 +42,10 @@ class UdpBlePassThroughHandler extends ScpMessageBaseHandle @override void handleResp(ScpMessage scpMessage) async { final BleResp bleResp = scpMessage.Payload; - AppLog.log('收到蓝牙消息回复:${bleResp.structData}'); - // 将 List 转换为十六进制字符串 String hexString = bleResp.structData .map((byte) => byte.toRadixString(16).padLeft(2, '0')) .join(' '); - AppLog.log('蓝牙透传回复:$hexString'); + AppLog.log('收到蓝牙透传回复:$hexString'); await CommandReciverManager.appDataReceive(bleResp.structData); @@ -57,8 +55,6 @@ class UdpBlePassThroughHandler extends ScpMessageBaseHandle AppLog.log('收到开门请求命令回复'); _replyOpenLock(reply); } - - }); } @@ -146,7 +142,7 @@ class UdpBlePassThroughHandler extends ScpMessageBaseHandle lockDetailLogic.lockReportLockSuccessfullyUploadData(); lockDetailLogic.resetOpenDoorState(); - lockDetailState.animationController!.stop(); + lockDetailState.animationController?.stop(); //锁数据更新 lockDetailLogic.getLockRecordLastUploadDataTime(); diff --git a/lib/talk/startChart/handle/impl/udp_rbcuInfo_handler.dart b/lib/talk/startChart/handle/impl/udp_rbcuInfo_handler.dart index ccdb7e42..63e532a8 100644 --- a/lib/talk/startChart/handle/impl/udp_rbcuInfo_handler.dart +++ b/lib/talk/startChart/handle/impl/udp_rbcuInfo_handler.dart @@ -3,7 +3,7 @@ import 'package:star_lock/talk/startChart/constant/message_type_constant.dart'; import 'package:star_lock/talk/startChart/entity/scp_message.dart'; import 'package:star_lock/talk/startChart/handle/scp_message_base_handle.dart'; import 'package:star_lock/talk/startChart/handle/scp_message_handle.dart'; -import 'package:star_lock/talk/startChart/p2p/p2p_manage.dart'; + import 'package:star_lock/talk/startChart/proto/generic.pb.dart'; import 'package:star_lock/talk/startChart/proto/rbcu.pb.dart'; import 'package:star_lock/talk/startChart/proto/talk_request.pb.dart'; @@ -68,7 +68,7 @@ class UdpRbcuInfoHandler extends ScpMessageBaseHandle /// 处理回复的rbcuInfo消息 void _handleResultRbcuInfo(RbcuInfo rbcuInfo) { - P2pManage().communicationObjectRbcuInfo = rbcuInfo; + startChartManage.rbcuInfo = rbcuInfo; startChartManage.stopSendingRbcuInfoMessages(); } } diff --git a/lib/talk/startChart/handle/impl/udp_rbcuProBe_handler.dart b/lib/talk/startChart/handle/impl/udp_rbcuProBe_handler.dart new file mode 100644 index 00000000..aea2b1cb --- /dev/null +++ b/lib/talk/startChart/handle/impl/udp_rbcuProBe_handler.dart @@ -0,0 +1,57 @@ +import 'package:star_lock/login/selectCountryRegion/common/index.dart'; +import 'package:star_lock/talk/startChart/constant/message_type_constant.dart'; +import 'package:star_lock/talk/startChart/entity/scp_message.dart'; +import 'package:star_lock/talk/startChart/handle/scp_message_base_handle.dart'; +import 'package:star_lock/talk/startChart/handle/scp_message_handle.dart'; + +import 'package:star_lock/talk/startChart/proto/generic.pb.dart'; +import 'package:star_lock/talk/startChart/proto/rbcu.pb.dart'; +import 'package:star_lock/talk/startChart/proto/talk_request.pb.dart'; + +class UdpRbcuProBeHandler extends ScpMessageBaseHandle + implements ScpMessageHandler { + @override + deserializePayload( + {required int payloadType, + required int messageType, + required List byte, + int? offset, + int? PayloadLength, + int? spTotal, + int? spIndex, + int? messageId}) { + if (messageType == MessageTypeConstant.Resp) { + final GenericResp genericResp = GenericResp(); + genericResp.mergeFromBuffer(byte); + return genericResp; + } else if (messageType == MessageTypeConstant.Req) { + final RbcuProbe rbcuProbe = RbcuProbe(); + rbcuProbe.mergeFromBuffer(byte); + return rbcuProbe; + } else { + String payload = utf8.decode(byte); + return payload; + } + } + + @override + void handleInvalidReq(ScpMessage scpMessage) { + // TODO: implement handleInvalidReq + } + + @override + void handleRealTimeData(ScpMessage scpMessage) { + // TODO: implement handleRealTimeData + } + + @override + void handleReq(ScpMessage scpMessage) { + // 收到rbcuprobe + final RbcuProbe rbcuProbe = scpMessage.Payload; + // 发送确认包 + startChartManage.rbcuProbe = rbcuProbe; + } + + @override + void handleResp(ScpMessage scpMessage) {} +} diff --git a/lib/talk/startChart/handle/impl/udp_talk_data_handler.dart b/lib/talk/startChart/handle/impl/udp_talk_data_handler.dart index b8551fcf..a70bc4f7 100644 --- a/lib/talk/startChart/handle/impl/udp_talk_data_handler.dart +++ b/lib/talk/startChart/handle/impl/udp_talk_data_handler.dart @@ -128,10 +128,10 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle /// 处理g711音频数据 void _handleVideoG711(TalkData talkData) { try { - final g711Data = talkData.content; - // 转pcm数据 - List pcmBytes = G711().convertList(g711Data); - talkData.content = pcmBytes; + // final g711Data = talkData.content; + // // 转pcm数据 + // List pcmBytes = G711().convertList(g711Data); + // talkData.content = pcmBytes; talkDataRepository.addTalkData(talkData); } catch (e) { print('Error decoding G.711 to PCM: $e'); @@ -142,34 +142,69 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle // 存储找到的所有完整帧 List frames = []; - // 寻找完整帧 (0xFFD8 开始, 0xFFD9 结束) - int startIdx = 0; - while (startIdx < payload.length - 1) { - // 找到帧的起始标志 0xFFD8 - startIdx = payload.indexOf(0xFF, startIdx); - if (startIdx == -1 || startIdx + 1 >= payload.length) break; - if (payload[startIdx + 1] == 0xD8) { - // 找到帧的起始标志 0xFFD8 - int endIdx = startIdx + 2; - while (endIdx < payload.length - 1) { - endIdx = payload.indexOf(0xFF, endIdx); - if (endIdx == -1 || endIdx + 1 >= payload.length) break; - if (payload[endIdx + 1] == 0xD9) { - // 找到帧的结束标志 0xFFD9 - Uint8List frame = payload.sublist(startIdx, endIdx + 2); - frames.add(frame); - startIdx = endIdx + 2; // 继续寻找下一个帧 + // 手动遍历 payload + int i = 0; + while (i < payload.length - 1) { + // 寻找起始标志 0xFFD8 + if (payload[i] == 0xFF && payload[i + 1] == 0xD8) { + int startIdx = i; + i += 2; // 跳过起始标志 + + // 寻找结束标志 0xFFD9 + while (i < payload.length - 1) { + if (payload[i] == 0xFF && payload[i + 1] == 0xD9) { + // 找到结束标志 0xFFD9 + int endIdx = i + 2; + // 使用 Uint8List.view 创建视图,避免内存分配 + frames.add( + Uint8List.view(payload.buffer, startIdx, endIdx - startIdx)); + i = endIdx; // 继续寻找下一个帧 break; } else { - endIdx += 1; // 继续寻找结束标志 + i += 1; // 继续寻找结束标志 } } } else { - startIdx += 1; // 继续寻找下一个起始标志 + i += 1; // 继续寻找起始标志 } } // 返回找到的所有完整帧 return frames; } + +// Future> _processCompletePayload(Uint8List payload) async { +// // 存储找到的所有完整帧 +// List frames = []; +// +// // 寻找完整帧 (0xFFD8 开始, 0xFFD9 结束) +// int startIdx = 0; +// while (startIdx < payload.length - 1) { +// // 找到帧的起始标志 0xFFD8 +// startIdx = payload.indexOf(0xFF, startIdx); +// if (startIdx == -1 || startIdx + 1 >= payload.length) break; +// if (payload[startIdx + 1] == 0xD8) { +// // 找到帧的起始标志 0xFFD8 +// int endIdx = startIdx + 2; +// while (endIdx < payload.length - 1) { +// endIdx = payload.indexOf(0xFF, endIdx); +// if (endIdx == -1 || endIdx + 1 >= payload.length) break; +// if (payload[endIdx + 1] == 0xD9) { +// // 找到帧的结束标志 0xFFD9 +// Uint8List frame = payload.sublist(startIdx, endIdx + 2); +// frames.add(frame); +// startIdx = endIdx + 2; // 继续寻找下一个帧 +// break; +// } else { +// endIdx += 1; // 继续寻找结束标志 +// } +// } +// } else { +// startIdx += 1; // 继续寻找下一个起始标志 +// } +// } +// +// // 返回找到的所有完整帧 +// return frames; +// } } diff --git a/lib/talk/startChart/handle/impl/udp_talk_hangup_handler.dart b/lib/talk/startChart/handle/impl/udp_talk_hangup_handler.dart index d4124e53..30f3178a 100644 --- a/lib/talk/startChart/handle/impl/udp_talk_hangup_handler.dart +++ b/lib/talk/startChart/handle/impl/udp_talk_hangup_handler.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:star_lock/talk/startChart/constant/message_type_constant.dart'; import 'package:star_lock/talk/startChart/constant/talk_status.dart'; import 'package:star_lock/talk/startChart/entity/scp_message.dart'; @@ -31,6 +32,8 @@ class UdpTalkHangUpHandler extends ScpMessageBaseHandle talkeRequestOverTimeTimerManager.cancel(); talkePingOverTimeTimerManager.cancel(); talkDataOverTimeTimerManager.cancel(); + + EasyLoading.showToast('已挂断'); } @override diff --git a/lib/talk/startChart/handle/other/talk_data_repository.dart b/lib/talk/startChart/handle/other/talk_data_repository.dart index 8eedbc3f..2a994bdd 100644 --- a/lib/talk/startChart/handle/other/talk_data_repository.dart +++ b/lib/talk/startChart/handle/other/talk_data_repository.dart @@ -4,7 +4,17 @@ import 'package:star_lock/talk/startChart/proto/talk_data.pb.dart'; class TalkDataRepository { // 创建一个私有的构造函数,防止外部创建实例 - TalkDataRepository._(); + TalkDataRepository._() { + _talkDataStreamController = StreamController.broadcast( + onListen: () { + _isListening = true; + }, + onCancel: () { + _isListening = false; + }, + sync: false, // 异步模式 + ); + } // 使用 _instance 来保存单例对象 static final TalkDataRepository _instance = TalkDataRepository._(); @@ -13,18 +23,37 @@ class TalkDataRepository { static TalkDataRepository get instance => _instance; // 创建一个 StreamController - final StreamController _talkDataStreamController = StreamController.broadcast(); + late final StreamController _talkDataStreamController; + + bool _isListening = false; // 提供一个方法来获取 Stream - Stream get talkDataStream => _talkDataStreamController.stream; + Stream get talkDataStream => + _talkDataStreamController.stream.transform( + StreamTransformer.fromHandlers( + handleData: (TalkData data, EventSink sink) { + // 限制缓冲区大小为 100 + if (_buffer.length >= 100) { + _buffer.removeAt(0); // 丢弃最旧的数据 + } + _buffer.add(data); + sink.add(data); + }, + ), + ); + final List _buffer = []; // 用于存储数据的缓冲区 // 提供一个方法来添加 TalkData 到 Stream void addTalkData(TalkData talkData) async { - _talkDataStreamController.add(talkData); + if (_isListening) { + Future.microtask(() { + _talkDataStreamController.add(talkData); + }); + } } // 提供一个方法来关闭 StreamController void dispose() { _talkDataStreamController.close(); } -} \ No newline at end of file +} diff --git a/lib/talk/startChart/handle/scp_message_handler_factory.dart b/lib/talk/startChart/handle/scp_message_handler_factory.dart index 1a57c7cb..73d5df19 100644 --- a/lib/talk/startChart/handle/scp_message_handler_factory.dart +++ b/lib/talk/startChart/handle/scp_message_handler_factory.dart @@ -57,6 +57,8 @@ class ScpMessageHandlerFactory { return UdpTalkHangUpHandler(); case PayloadTypeConstant.RbcuInfo: return UdpRbcuInfoHandler(); + case PayloadTypeConstant.RbcuProbe: + return UdpRbcuInfoHandler(); default: return UnKnowPayloadTypeHandler(); } diff --git a/lib/talk/startChart/p2p/p2p_manage.dart b/lib/talk/startChart/p2p/p2p_manage.dart deleted file mode 100644 index 54e3e1a7..00000000 --- a/lib/talk/startChart/p2p/p2p_manage.dart +++ /dev/null @@ -1,108 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:star_lock/talk/startChart/proto/rbcu.pb.dart'; - -class P2pManage { - // 常量定义 - static const String testMessage = 'Hello'; // 测试消息 - static const int localPort = 0; // 绑定本地端口(0 表示由系统分配) - static const Duration connectionTimeout = Duration(seconds: 5); // 连接超时时间 - static const List localNetworkPrefixes = [ - '192.168.', - '10.', - '172.' - ]; // 局域网 IP 前缀 - - RbcuInfo? communicationObjectRbcuInfo; - - void init() { - // 初始化逻辑 - } - - // 解析 address 属性,提取对方的 IP 和端口 - List> parseRemoteAddresses() { - final addresses = communicationObjectRbcuInfo?.address ?? []; - return addresses.map((addr) { - final parts = addr.split(':'); - return {'ip': parts[0], 'port': parts[1]}; - }).toList(); - } - - // 判断是否为局域网 IP - bool _isLocalNetworkIp(String ip) { - for (final prefix in localNetworkPrefixes) { - if (ip.startsWith(prefix)) { - // 处理 172.16.x.x 到 172.31.x.x - if (prefix == '172.') { - final secondOctet = int.tryParse(ip.split('.')[1]) ?? 0; - if (secondOctet >= 16 && secondOctet <= 31) { - return true; - } - } else { - return true; - } - } - } - return false; - } - - // 创建 UDP 连接 - Future createUdpConnection() async { - final addresses = parseRemoteAddresses(); - - // 优先使用局域网 IP - addresses.sort((a, b) { - final isALocal = _isLocalNetworkIp(a['ip']!); - final isBLocal = _isLocalNetworkIp(b['ip']!); - if (isALocal && !isBLocal) return -1; // a 是局域网 IP,优先级更高 - if (!isALocal && isBLocal) return 1; // b 是局域网 IP,优先级更高 - return 0; // 优先级相同 - }); - - // 尝试连接 - for (final addr in addresses) { - final ip = addr['ip']!; - final port = int.parse(addr['port']!); - - try { - final socket = await RawDatagramSocket.bind( - InternetAddress.anyIPv4, localPort); // 绑定本地端口 - - // 发送测试数据 - final testData = testMessage.codeUnits; - socket.send(testData, InternetAddress(ip), port); - - // 设置超时时间 - final completer = Completer(); - - // 监听响应 - socket.listen((event) { - if (event == RawSocketEvent.read) { - final datagram = socket.receive(); - if (datagram != null) { - final response = String.fromCharCodes(datagram.data); - print('收到来自 $ip:$port 的响应: $response'); - completer.complete(); // 收到响应,完成 Future - } - } - }); - - // 等待响应或超时 - try { - await completer.future.timeout(connectionTimeout); - print('成功连接到 $ip:$port'); - return; // 连接成功,退出循环 - } on TimeoutException { - print('连接到 $ip:$port 超时'); - } finally { - socket.close(); // 关闭 socket - } - } catch (e) { - print('连接到 $ip:$port 失败: $e'); - } - } - - print('无法连接到任何地址'); - } -} diff --git a/lib/talk/startChart/start_chart_manage.dart b/lib/talk/startChart/start_chart_manage.dart index 52c91dad..28c9551d 100644 --- a/lib/talk/startChart/start_chart_manage.dart +++ b/lib/talk/startChart/start_chart_manage.dart @@ -1,5 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math'; +import 'dart:typed_data'; import 'package:fixnum/fixnum.dart'; import 'package:get/get.dart'; @@ -29,6 +31,7 @@ import 'package:star_lock/talk/startChart/handle/other/talke_request_over_time_t import 'package:star_lock/talk/startChart/handle/scp_message_handle.dart'; import 'package:star_lock/talk/startChart/handle/scp_message_handler_factory.dart'; import 'package:star_lock/talk/startChart/proto/rbcu.pb.dart'; +import 'package:star_lock/talk/startChart/proto/rbcu.pbserver.dart'; import 'package:star_lock/talk/startChart/proto/talk_data.pb.dart'; import 'package:star_lock/talk/startChart/proto/talk_expect.pb.dart'; import 'package:star_lock/talk/startChart/proto/talk_expect.pbserver.dart'; @@ -86,9 +89,14 @@ class StartChartManage { int _defaultIntervalTime = 1; // 默认定时发送间隔(s) Timer? talkDataTimer; // 发送通话数据消息定时器 Timer? rbcuInfoTimer; // p2p地址交换定时器 + Timer? rbcuProbeTimer; // p2p打洞包 + Timer? rbcuConfirmTimer; // p2p打洞确认包 + String _rbcuSessionId = ''; // p2p SessionId Timer? talkRequestTimer; // 对讲请求定时器 final int maxAttempts = 15; // 最大执行次数 - + RbcuInfo? rbcuInfo; + RbcuProbe? rbcuProbe; + RbcuConfirm? rbcuConfirm; final int _maxPayloadSize = 8 * 1024; // 分包大小 // 默认通话的期望数据格式 @@ -254,6 +262,7 @@ class StartChartManage { ) // 去除 "udp://" 前缀 .cast< String>(); // 转换为 Iterable// 将 Iterable 转换为 Iterable + _rbcuSessionId = uuid; final RbcuInfo rbcuInfo = RbcuInfo( sessionId: uuid, name: uuid, @@ -261,6 +270,7 @@ class StartChartManage { time: int64Timestamp, isResp: isResp, ); + final message = MessageCommand.genericRbcuInfoMessage( ToPeerId: ToPeerId, FromPeerId: FromPeerId, @@ -270,6 +280,44 @@ class StartChartManage { _sendMessage(message: message); } + // 发送RbcuProbe + void _sendRbcuProbeMessage() async { + // 随机字符串数据 + String generateRandomString(int length) { + const chars = + 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + final random = Random(); + return String.fromCharCodes( + List.generate( + length, (index) => chars.codeUnitAt(random.nextInt(chars.length))), + ); + } + + final info = rbcuInfo = RbcuInfo(address: ['119.137.55.161:62289']); + if (info != null && info.address != null && info.address.length > 0) { + info.address.forEach((element) { + // 拆分 element 字符串 + final parts = element.split(':'); + final host = parts[0]; // IP 地址 + final port = int.tryParse(parts[1]) ?? 0; // 端口号,如果解析失败则默认为 0 + + final RbcuProbe rbcuProbe = RbcuProbe( + sessionId: _rbcuSessionId, + data: generateRandomString(100), + targetAddress: element); + final rbcuProBeBuffer = rbcuProbe.writeToBuffer(); + _sendNatMessage(message: rbcuProBeBuffer, host: host, port: port); + }); + } + } + + // 发送RbcuConfirm 打洞确认 + void _sendRbcuConfirmMessage() async { + RbcuConfirm( + sessionId: _rbcuSessionId, + ); + } + // 启动定时任务 void startSendingRbcuInfoMessages({required String ToPeerId}) { // 每隔 1 秒执行一次 _sendRbcuInfoMessage @@ -280,12 +328,43 @@ class StartChartManage { }); } + // 发送打洞包 + void startSendingRbcuProbeTMessages() { + // 每隔 1 秒执行一次 _sendRbcuInfoMessage + rbcuProbeTimer ??= + Timer.periodic(Duration(seconds: _defaultIntervalTime), (timer) { + // 发送RbcuProbe + _sendRbcuProbeMessage(); + }); + } + + // 发送打洞确认包 + void startSendingRbcuConfirmTMessages() { + rbcuConfirmTimer ??= + Timer.periodic(Duration(seconds: _defaultIntervalTime), (timer) { + // 发送RbcuProbe + _sendRbcuConfirmMessage(); + }); + } + // 停止定时任务 void stopSendingRbcuInfoMessages() { rbcuInfoTimer?.cancel(); // 取消定时器 rbcuInfoTimer = null; } + // 停止定时任务 + void stopSendingRbcuProBeMessages() { + rbcuProbeTimer?.cancel(); // 取消定时器 + rbcuProbeTimer = null; + } + + // 停止定时任务 + void stopSendingRbcuConfirmMessages() { + rbcuConfirmTimer?.cancel(); // 取消定时器 + rbcuConfirmTimer = null; + } + // 回复RbcuInfo void replyRbcuInfoMessage({required String ToPeerId}) { _sendRbcuInfoMessage(ToPeerId: ToPeerId, isResp: true); @@ -345,7 +424,9 @@ class StartChartManage { } // 发送对讲数据 + // 现在的场景只有给锁板发送音频数据 Future sendTalkDataMessage({required TalkData talkData}) async { + String toPeerId = lockPeerId; final List payload = talkData.content; // 计算需要分多少个包发送 final int totalPackets = (payload.length / _maxPayloadSize).ceil(); @@ -361,10 +442,10 @@ class StartChartManage { // 分包数据不递增messageID final messageId = - MessageCommand.getNextMessageId(ToPeerId, increment: false); + MessageCommand.getNextMessageId(toPeerId, increment: false); // 组装分包数据 final message = MessageCommand.talkDataMessage( - ToPeerId: 'D78Fo4CjNzUXz8DxuUhLtcRpnGFXhSzhzs191XzhJttS', + ToPeerId: toPeerId, FromPeerId: FromPeerId, payload: packet, SpTotal: totalPackets, @@ -375,7 +456,7 @@ class StartChartManage { await _sendMessage(message: message); } // 分包发送完了递增一下id - MessageCommand.getNextMessageId(ToPeerId); + MessageCommand.getNextMessageId(toPeerId); } // 发送心跳包消息 @@ -603,6 +684,20 @@ class StartChartManage { } } + // 发送消息 + Future _sendNatMessage( + {required List message, + required String host, + required int port}) async { + _log(text: '发送nat消息'); + var result = await _udpSocket?.send(message, InternetAddress(host), port); + if (result != message.length) { + throw StartChartMessageException( + '❌Udp send data error----> $result ${message.length}'); + // _udpSocket = null; + } + } + // 请求注册节点 Future _requestStarChartRegisterNode() async { // 获取设备信息 @@ -960,6 +1055,8 @@ class StartChartManage { stopReStartOnlineStartChartServer(); stopTalkDataTimer(); stopSendingRbcuInfoMessages(); + stopSendingRbcuProBeMessages(); + stopSendingRbcuConfirmMessages(); _resetData(); await Storage.removerRelayInfo(); await Storage.removerStarChartRegisterNodeInfo(); diff --git a/lib/talk/startChart/views/talkView/talk_view_logic.dart b/lib/talk/startChart/views/talkView/talk_view_logic.dart index 93a7f1b9..98d042c0 100644 --- a/lib/talk/startChart/views/talkView/talk_view_logic.dart +++ b/lib/talk/startChart/views/talkView/talk_view_logic.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/rendering.dart'; @@ -23,6 +24,7 @@ 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/network/api_repository.dart'; +import 'package:star_lock/talk/call/g711.dart'; import 'package:star_lock/talk/startChart/constant/talk_status.dart'; import 'package:star_lock/talk/startChart/proto/talk_data.pb.dart'; @@ -43,18 +45,18 @@ class TalkViewLogic extends BaseGetXController { final LockDetailState lockDetailState = Get.put(LockDetailLogic()).state; Timer? _syncTimer; // 音视频播放刷新率定时器 int _startTime = 0; // 开始播放时间戳,用于判断帧数据中的时间戳位置 - final int bufferSize = 20; // 缓冲区大小(以帧为单位) + int bufferSize = 20; // 缓冲区大小(以帧为单位) final List frameTimestamps = []; // 帧时间戳用于计算 FPS int frameIntervalMs = 45; // 初始帧间隔设置为45毫秒(约22FPS) int minFrameIntervalMs = 30; // 最小帧间隔(约33 FPS) - int maxFrameIntervalMs = 500; // 最大帧间隔(约1 FPS) + int maxFrameIntervalMs = 100; // 最大帧间隔(约1 FPS) // int maxFrameIntervalMs = 100; // 最大帧间隔(约10 FPS) /// 初始化音频播放器 void _initFlutterPcmSound() { - const int sampleRate = 44100; - FlutterPcmSound.setLogLevel(LogLevel.verbose); - FlutterPcmSound.setup(sampleRate: sampleRate, channelCount: 2); + const int sampleRate = 8000; + FlutterPcmSound.setLogLevel(LogLevel.none); + FlutterPcmSound.setup(sampleRate: sampleRate, channelCount: 1); // 设置 feed 阈值 if (Platform.isAndroid) { FlutterPcmSound.setFeedThreshold(-1); // Android 平台的特殊处理 @@ -82,24 +84,22 @@ class TalkViewLogic extends BaseGetXController { // 监听音视频数据流 void _startListenTalkData() { - state.talkDataRepository.talkDataStream.listen((TalkData talkData) { + state.talkDataRepository.talkDataStream.listen((TalkData talkData) async { final contentType = talkData.contentType; - + int currentTimestamp = DateTime.now().millisecondsSinceEpoch; // 判断数据类型,进行分发处理 switch (contentType) { case TalkData_ContentTypeE.G711: - if (state.audioBuffer.length < bufferSize) { - state.audioBuffer.add(talkData); + if (state.audioBuffer.length >= bufferSize) { + state.audioBuffer.removeAt(0); // 丢弃最旧的数据 } - // print('收到音频数据'); - + state.audioBuffer.add(talkData); // 添加新数据 break; case TalkData_ContentTypeE.Image: - if (state.videoBuffer.length < bufferSize) { - state.videoBuffer.add(talkData); + if (state.videoBuffer.length >= bufferSize) { + state.videoBuffer.removeAt(0); // 丢弃最旧的数据 } - // print('talkData durationMs-->:${talkData.durationMs}'); - + state.videoBuffer.add(talkData); // 添加新数据 /// 更新网络状态 // updateNetworkStatus(currentTimestamp); break; @@ -127,9 +127,11 @@ class TalkViewLogic extends BaseGetXController { } /// 播放音频数据 - void _playAudioData(TalkData talkData) { + void _playAudioData(TalkData talkData) async { + final list = G711().decodeAndDenoise(talkData.content, true, 8000, 100, 50); + // 将 PCM 数据转换为 PcmArrayInt16 - final PcmArrayInt16 fromList = PcmArrayInt16.fromList(talkData.content); + final PcmArrayInt16 fromList = PcmArrayInt16.fromList(list); FlutterPcmSound.feed(fromList); if (!state.isPlaying.value) { FlutterPcmSound.play(); @@ -138,7 +140,7 @@ class TalkViewLogic extends BaseGetXController { } /// 播放视频数据 - void _playVideoData(TalkData talkData) { + void _playVideoData(TalkData talkData) async { state.listData.value = Uint8List.fromList(talkData.content); } @@ -153,8 +155,6 @@ class TalkViewLogic extends BaseGetXController { final currentTime = DateTime.now().millisecondsSinceEpoch; final elapsedTime = currentTime - _startTime; - // 根据 elapsedTime 同步音频和视频 - // AppLog.log('Elapsed Time: $elapsedTime ms'); // 动态调整帧间隔 _adjustFrameInterval(); @@ -163,6 +163,7 @@ class TalkViewLogic extends BaseGetXController { state.audioBuffer.first.durationMs <= elapsedTime) { // 判断音频开关是否打开 if (state.isOpenVoice.value) { + AppLog.log('播放音频:${state.audioBuffer[0]}'); _playAudioData(state.audioBuffer.removeAt(0)); } else { // 如果不播放音频,只从缓冲区中读取数据,但不移除 @@ -189,6 +190,13 @@ class TalkViewLogic extends BaseGetXController { /// 动态调整帧间隔 void _adjustFrameInterval() { + int newFrameIntervalMs = frameIntervalMs; + if (state.networkStatus.value == NetworkStatus.lagging) { + bufferSize = 30; // 增大缓冲区 + } else { + bufferSize = 20; // 恢复默认缓冲区大小 + } + if (state.videoBuffer.length < 10 && frameIntervalMs < maxFrameIntervalMs) { // 如果缓冲区较小且帧间隔小于最大值,则增加帧间隔 frameIntervalMs += 5; @@ -197,38 +205,47 @@ class TalkViewLogic extends BaseGetXController { // 如果缓冲区较大且帧间隔大于最小值,则减少帧间隔 frameIntervalMs -= 5; } - _syncTimer?.cancel(); - _syncTimer = - Timer.periodic(Duration(milliseconds: frameIntervalMs), (timer) { - final currentTime = DateTime.now().millisecondsSinceEpoch; - final elapsedTime = currentTime - _startTime; + // 只有在帧间隔发生变化时才重建定时器 + if (newFrameIntervalMs != frameIntervalMs) { + frameIntervalMs = newFrameIntervalMs; + // 取消旧的定时器 + _syncTimer?.cancel(); + _syncTimer = + Timer.periodic(Duration(milliseconds: frameIntervalMs), (timer) { + final currentTime = DateTime.now().millisecondsSinceEpoch; + final elapsedTime = currentTime - _startTime; - // 播放合适的音频帧 - if (state.audioBuffer.isNotEmpty && - state.audioBuffer.first.durationMs <= elapsedTime) { - // 判断音频开关是否打开 - if (state.isOpenVoice.value) { - _playAudioData(state.audioBuffer.removeAt(0)); - } else { - // 如果不播放音频,只从缓冲区中读取数据,但不移除 - // 你可以根据需要调整此处逻辑,例如保留缓冲区的最大长度,防止无限增长 - // 仅移除缓冲区数据但不播放音频,确保音频也是实时更新的 - state.audioBuffer.removeAt(0); + // 播放合适的音频帧 + if (state.audioBuffer.isNotEmpty && + state.audioBuffer.first.durationMs <= elapsedTime) { + // 判断音频开关是否打开 + if (state.isOpenVoice.value) { + _playAudioData(state.audioBuffer.removeAt(0)); + } else { + // 如果不播放音频,只从缓冲区中读取数据,但不移除 + // 你可以根据需要调整此处逻辑,例如保留缓冲区的最大长度,防止无限增长 + // 仅移除缓冲区数据但不播放音频,确保音频也是实时更新的 + state.audioBuffer.removeAt(0); + } } - } - // 播放合适的视频帧 - // 跳帧策略:如果缓冲区中有多个帧,且它们的时间戳都在当前时间之前,则播放最新的帧 - while (state.videoBuffer.isNotEmpty && - state.videoBuffer.first.durationMs <= elapsedTime) { - // 如果有多个帧,移除旧的帧,保持最新的帧 - if (state.videoBuffer.length > 1) { - state.videoBuffer.removeAt(0); - } else { - _playVideoData(state.videoBuffer.removeAt(0)); + // 播放合适的视频帧 + // 跳帧策略:如果缓冲区中有多个帧,且它们的时间戳都在当前时间之前,则播放最新的帧 + int maxFramesToProcess = 5; // 每次最多处理 5 帧 + int processedFrames = 0; + + while (state.videoBuffer.isNotEmpty && + state.videoBuffer.first.durationMs <= elapsedTime && + processedFrames < maxFramesToProcess) { + if (state.videoBuffer.length > 1) { + state.videoBuffer.removeAt(0); + } else { + _playVideoData(state.videoBuffer.removeAt(0)); + } + processedFrames++; } - } - }); + }); + } } /// 修改网络状态 @@ -238,11 +255,11 @@ class TalkViewLogic extends BaseGetXController { if (frameInterval > 500 && frameInterval <= 1000) { // 判断帧间隔是否在500毫秒到1秒之间 state.networkStatus.value = NetworkStatus.lagging; - showNetworkStatus("Network is lagging"); + // showNetworkStatus("Network is lagging"); } else if (frameInterval > 1000) { // 判断帧间隔是否超过1秒 state.networkStatus.value = NetworkStatus.delayed; - showNetworkStatus("Network is delayed"); + // showNetworkStatus("Network is delayed"); } else { state.networkStatus.value = NetworkStatus.normal; state.alertCount.value = 0; // 重置计数器 @@ -436,12 +453,13 @@ class TalkViewLogic extends BaseGetXController { @override void onClose() { - _stopPlayG711Data(); - state.listData.value = Uint8List(0); - state.audioBuffer.clear(); - state.videoBuffer.clear(); - _syncTimer?.cancel(); - _syncTimer = null; + _stopPlayG711Data(); // 停止播放音频 + state.listData.value = Uint8List(0); // 清空视频数据 + state.audioBuffer.clear(); // 清空音频缓冲区 + state.videoBuffer.clear(); // 清空视频缓冲区 + _syncTimer?.cancel(); // 取消定时器 + _syncTimer = null; // 释放定时器引用 + super.onClose(); } /// 处理无效通话状态 @@ -563,7 +581,7 @@ class TalkViewLogic extends BaseGetXController { // _concatenateFrames(state.recordingAudioAllFrames); // 连接所有帧 final List pcmBytes = _listLinearToULaw(frame); // 发送音频数据 - StartChartManage().sendTalkDataMessage( + await StartChartManage().sendTalkDataMessage( talkData: TalkData( content: pcmBytes, contentType: TalkData_ContentTypeE.G711, @@ -571,6 +589,7 @@ class TalkViewLogic extends BaseGetXController { state.startRecordingAudioTime.value.millisecondsSinceEpoch, ), ); + AppLog.log('发送音频数据'); } void _onError(VoiceProcessorException error) { diff --git a/lib/talk/startChart/views/talkView/talk_view_page.dart b/lib/talk/startChart/views/talkView/talk_view_page.dart index 5f796220..dac55d6d 100644 --- a/lib/talk/startChart/views/talkView/talk_view_page.dart +++ b/lib/talk/startChart/views/talkView/talk_view_page.dart @@ -168,7 +168,36 @@ class _TalkViewPageState extends State // ), Obx(() => state.listData.value.isEmpty ? buildRotationTransition() - : Container()) + : 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( + '正在说话...', + style: + TextStyle(fontSize: 20.sp, color: Colors.white), + ), + ], + ), + ), + ), + ) + : Container()), ], ), ); @@ -273,11 +302,13 @@ class _TalkViewPageState extends State if (state.talkStatus.value == TalkStatus.answeredSuccessfully) { print('开始录音'); logic.startProcessingAudio(); + state.isLongPressing.value = true; } }, longPressUp: () async { print('停止录音'); logic.stopProcessingAudio(); + state.isLongPressing.value = false; }, onClick: () async { if (state.talkStatus.value == @@ -300,8 +331,8 @@ class _TalkViewPageState extends State AppColors.mainColor, onClick: () { // if (UDPManage().remoteUnlock == 1) { - logic.udpOpenDoorAction(); - // showDeletPasswordAlertDialog(context); + logic.udpOpenDoorAction(); + // showDeletPasswordAlertDialog(context); // } else { // logic.showToast('请在锁设置中开启远程开锁'.tr); // } @@ -334,36 +365,43 @@ class _TalkViewPageState extends State } } - Widget bottomBtnItemWidget(String iconUrl, String name, Color backgroundColor, - {required Function() onClick, - Function()? longPress, - Function()? longPressUp}) { + Widget bottomBtnItemWidget( + String iconUrl, + String name, + Color backgroundColor, { + required Function() onClick, + Function()? longPress, + Function()? longPressUp, + }) { double wh = 80.w; return GestureDetector( onTap: onClick, onLongPress: longPress, onLongPressUp: longPressUp, child: SizedBox( - height: 140.h, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - width: wh, - height: wh, - decoration: BoxDecoration( - color: backgroundColor, - borderRadius: BorderRadius.circular((wh + 10.w * 2) / 2)), - padding: EdgeInsets.all(20.w), - child: Image.asset(iconUrl, fit: BoxFit.fitWidth), + height: 140.h, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: wh, + height: wh, + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular((wh + 10.w * 2) / 2), ), - SizedBox(height: 20.w), - Expanded( - child: Text(name, - style: TextStyle(fontSize: 20.sp, color: Colors.white), - textAlign: TextAlign.center)) - ], - )), + padding: EdgeInsets.all(20.w), + child: Image.asset(iconUrl, fit: BoxFit.fitWidth), + ), + SizedBox(height: 20.w), + Expanded( + child: Text(name, + style: TextStyle(fontSize: 20.sp, color: Colors.white), + textAlign: TextAlign.center), + ) + ], + ), + ), ); } diff --git a/lib/talk/startChart/views/talkView/talk_view_state.dart b/lib/talk/startChart/views/talkView/talk_view_state.dart index 05029877..18a1da89 100644 --- a/lib/talk/startChart/views/talkView/talk_view_state.dart +++ b/lib/talk/startChart/views/talkView/talk_view_state.dart @@ -6,6 +6,7 @@ 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/startChart/constant/talk_status.dart'; import 'package:star_lock/talk/startChart/handle/other/talk_data_repository.dart'; @@ -54,7 +55,13 @@ class TalkViewState { // 星图对讲相关状态 List audioBuffer = [].obs; + List audioBuffer2 = [].obs; + List activeAudioBuffer =[].obs; + List activeVideoBuffer =[].obs; + + List videoBuffer = [].obs; + List videoBuffer2 = [].obs; RxBool isPlaying = false.obs; // 是否开始播放 Rx talkStatus = TalkStatus.none.obs; //星图对讲状态 // 获取 startChartTalkStatus 的唯一实例 @@ -80,4 +87,5 @@ class TalkViewState { final int sampleRate = 8000; //录音频采样率为8000 List> recordingAudioAllFrames = >[]; // 录制音频的所有帧 RxInt rotateAngle = 0.obs; // 旋转角度(以弧度为单位) + RxBool isLongPressing = false.obs; // 旋转角度(以弧度为单位) }