fix:增加p2p消息协议代码、优化对讲时应用卡死、调整StreamController处理数据时的逻辑、调整在寻找图片帧时的代码逻辑
This commit is contained in:
parent
641ff4a2b6
commit
7a0e8f9e28
@ -2745,7 +2745,9 @@ class ApiProvider extends BaseProvider {
|
||||
jsonEncode({
|
||||
'deviceType': deviceType,
|
||||
'deviceMac': deviceMac,
|
||||
}));
|
||||
}),
|
||||
isShowNetworkErrorMsg: false,
|
||||
isUnShowLoading: true);
|
||||
}
|
||||
|
||||
extension ExtensionString on String {
|
||||
|
||||
@ -2739,4 +2739,7 @@ class ApiRepository {
|
||||
);
|
||||
return DeviceNetwork.fromJson(res.body);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -42,12 +42,10 @@ class UdpBlePassThroughHandler extends ScpMessageBaseHandle
|
||||
@override
|
||||
void handleResp(ScpMessage scpMessage) async {
|
||||
final BleResp bleResp = scpMessage.Payload;
|
||||
AppLog.log('收到蓝牙消息回复:${bleResp.structData}');
|
||||
// 将 List<int> 转换为十六进制字符串
|
||||
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();
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
57
lib/talk/startChart/handle/impl/udp_rbcuProBe_handler.dart
Normal file
57
lib/talk/startChart/handle/impl/udp_rbcuProBe_handler.dart
Normal file
@ -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<int> 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) {}
|
||||
}
|
||||
@ -128,10 +128,10 @@ class UdpTalkDataHandler extends ScpMessageBaseHandle
|
||||
/// 处理g711音频数据
|
||||
void _handleVideoG711(TalkData talkData) {
|
||||
try {
|
||||
final g711Data = talkData.content;
|
||||
// 转pcm数据
|
||||
List<int> pcmBytes = G711().convertList(g711Data);
|
||||
talkData.content = pcmBytes;
|
||||
// final g711Data = talkData.content;
|
||||
// // 转pcm数据
|
||||
// List<int> 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<Uint8List> 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<List<Uint8List>> _processCompletePayload(Uint8List payload) async {
|
||||
// // 存储找到的所有完整帧
|
||||
// List<Uint8List> 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;
|
||||
// }
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -4,7 +4,17 @@ import 'package:star_lock/talk/startChart/proto/talk_data.pb.dart';
|
||||
|
||||
class TalkDataRepository {
|
||||
// 创建一个私有的构造函数,防止外部创建实例
|
||||
TalkDataRepository._();
|
||||
TalkDataRepository._() {
|
||||
_talkDataStreamController = StreamController<TalkData>.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<TalkData> _talkDataStreamController = StreamController<TalkData>.broadcast();
|
||||
late final StreamController<TalkData> _talkDataStreamController;
|
||||
|
||||
bool _isListening = false;
|
||||
|
||||
// 提供一个方法来获取 Stream
|
||||
Stream<TalkData> get talkDataStream => _talkDataStreamController.stream;
|
||||
Stream<TalkData> get talkDataStream =>
|
||||
_talkDataStreamController.stream.transform(
|
||||
StreamTransformer<TalkData, TalkData>.fromHandlers(
|
||||
handleData: (TalkData data, EventSink<TalkData> sink) {
|
||||
// 限制缓冲区大小为 100
|
||||
if (_buffer.length >= 100) {
|
||||
_buffer.removeAt(0); // 丢弃最旧的数据
|
||||
}
|
||||
_buffer.add(data);
|
||||
sink.add(data);
|
||||
},
|
||||
),
|
||||
);
|
||||
final List<TalkData> _buffer = []; // 用于存储数据的缓冲区
|
||||
|
||||
// 提供一个方法来添加 TalkData 到 Stream
|
||||
void addTalkData(TalkData talkData) async {
|
||||
_talkDataStreamController.add(talkData);
|
||||
if (_isListening) {
|
||||
Future.microtask(() {
|
||||
_talkDataStreamController.add(talkData);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 提供一个方法来关闭 StreamController
|
||||
void dispose() {
|
||||
_talkDataStreamController.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,6 +57,8 @@ class ScpMessageHandlerFactory {
|
||||
return UdpTalkHangUpHandler();
|
||||
case PayloadTypeConstant.RbcuInfo:
|
||||
return UdpRbcuInfoHandler();
|
||||
case PayloadTypeConstant.RbcuProbe:
|
||||
return UdpRbcuInfoHandler();
|
||||
default:
|
||||
return UnKnowPayloadTypeHandler();
|
||||
}
|
||||
|
||||
@ -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<String> localNetworkPrefixes = [
|
||||
'192.168.',
|
||||
'10.',
|
||||
'172.'
|
||||
]; // 局域网 IP 前缀
|
||||
|
||||
RbcuInfo? communicationObjectRbcuInfo;
|
||||
|
||||
void init() {
|
||||
// 初始化逻辑
|
||||
}
|
||||
|
||||
// 解析 address 属性,提取对方的 IP 和端口
|
||||
List<Map<String, String>> 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<void> 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<void>();
|
||||
|
||||
// 监听响应
|
||||
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('无法连接到任何地址');
|
||||
}
|
||||
}
|
||||
@ -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<String>// 将 Iterable<String?> 转换为 Iterable<String>
|
||||
_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<void> sendTalkDataMessage({required TalkData talkData}) async {
|
||||
String toPeerId = lockPeerId;
|
||||
final List<int> 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<void> _sendNatMessage(
|
||||
{required List<int> 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<StarChartRegisterNodeEntity> _requestStarChartRegisterNode() async {
|
||||
// 获取设备信息
|
||||
@ -960,6 +1055,8 @@ class StartChartManage {
|
||||
stopReStartOnlineStartChartServer();
|
||||
stopTalkDataTimer();
|
||||
stopSendingRbcuInfoMessages();
|
||||
stopSendingRbcuProBeMessages();
|
||||
stopSendingRbcuConfirmMessages();
|
||||
_resetData();
|
||||
await Storage.removerRelayInfo();
|
||||
await Storage.removerStarChartRegisterNodeInfo();
|
||||
|
||||
@ -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<int> 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<int> 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) {
|
||||
|
||||
@ -168,7 +168,36 @@ class _TalkViewPageState extends State<TalkViewPage>
|
||||
// ),
|
||||
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<TalkViewPage>
|
||||
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<TalkViewPage>
|
||||
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<TalkViewPage>
|
||||
}
|
||||
}
|
||||
|
||||
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: <Widget>[
|
||||
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: <Widget>[
|
||||
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),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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<TalkData> audioBuffer = <TalkData>[].obs;
|
||||
List<TalkData> audioBuffer2 = <TalkData>[].obs;
|
||||
List<TalkData> activeAudioBuffer =<TalkData>[].obs;
|
||||
List<TalkData> activeVideoBuffer =<TalkData>[].obs;
|
||||
|
||||
|
||||
List<TalkData> videoBuffer = <TalkData>[].obs;
|
||||
List<TalkData> videoBuffer2 = <TalkData>[].obs;
|
||||
RxBool isPlaying = false.obs; // 是否开始播放
|
||||
Rx<TalkStatus> talkStatus = TalkStatus.none.obs; //星图对讲状态
|
||||
// 获取 startChartTalkStatus 的唯一实例
|
||||
@ -80,4 +87,5 @@ class TalkViewState {
|
||||
final int sampleRate = 8000; //录音频采样率为8000
|
||||
List<List<int>> recordingAudioAllFrames = <List<int>>[]; // 录制音频的所有帧
|
||||
RxInt rotateAngle = 0.obs; // 旋转角度(以弧度为单位)
|
||||
RxBool isLongPressing = false.obs; // 旋转角度(以弧度为单位)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user