961 lines
31 KiB
Dart
961 lines
31 KiB
Dart
import 'dart:async';
|
||
import 'dart:io';
|
||
import 'dart:typed_data';
|
||
|
||
import 'package:convert/convert.dart';
|
||
import 'package:fast_rsa/fast_rsa.dart' as fastRsa;
|
||
import 'package:flutter/services.dart';
|
||
import 'package:get/get.dart';
|
||
import 'package:pointycastle/export.dart' as pc;
|
||
import 'package:star_lock/app_settings/app_settings.dart';
|
||
import 'package:star_lock/flavors.dart';
|
||
import 'package:star_lock/login/login/entity/LoginEntity.dart';
|
||
import 'package:star_lock/network/api_repository.dart';
|
||
import 'package:star_lock/network/start_chart_api.dart';
|
||
import 'package:star_lock/talk/startChart/command/message_command.dart';
|
||
import 'package:star_lock/talk/startChart/constant/ip_constant.dart';
|
||
import 'package:star_lock/talk/startChart/constant/listen_addr_type_constant.dart';
|
||
import 'package:star_lock/talk/startChart/constant/message_type_constant.dart';
|
||
import 'package:star_lock/talk/startChart/constant/payload_type_constant.dart';
|
||
import 'package:star_lock/talk/startChart/entity/relay_info_entity.dart';
|
||
import 'package:star_lock/talk/startChart/entity/report_information_data.dart';
|
||
import 'package:star_lock/talk/startChart/entity/scp_message.dart';
|
||
import 'package:star_lock/talk/startChart/entity/star_chart_register_node_entity.dart';
|
||
import 'package:star_lock/talk/startChart/handle/scp_message_handle.dart';
|
||
import 'package:star_lock/talk/startChart/handle/scp_message_handler_factory.dart';
|
||
import 'package:star_lock/talk/startChart/proto/talk_data.pb.dart';
|
||
import 'package:star_lock/talk/startChart/proto/talk_expect.pb.dart';
|
||
import 'package:star_lock/talk/startChart/start_chart_talk_status.dart';
|
||
import 'package:star_lock/tools/baseGetXController.dart';
|
||
import 'package:star_lock/tools/deviceInfo_utils.dart';
|
||
import 'package:star_lock/tools/storage.dart';
|
||
import 'package:uuid/uuid.dart';
|
||
|
||
import 'dart:convert';
|
||
import 'package:asn1lib/asn1lib.dart' as asn1lib; // Prefix for asn1lib
|
||
|
||
class StartChartManage {
|
||
// 私有构造函数,防止外部直接new对象
|
||
StartChartManage._internal();
|
||
|
||
// 单例对象
|
||
static final StartChartManage _instance = StartChartManage._internal();
|
||
|
||
// 工厂构造函数,返回单例对象
|
||
factory StartChartManage() {
|
||
return _instance;
|
||
}
|
||
|
||
// 产品昵称
|
||
final String _productName = F.navTitle;
|
||
|
||
RawDatagramSocket? _udpSocket;
|
||
final Map<String, Completer<void>> _completers = {}; // 发送消息的请求是否完成
|
||
final Uuid _uuid = Uuid(); // 用于区分发送的消息的唯一id
|
||
int _messageMaxTimeout = 5; // 消息最大超时时间(s)
|
||
late String remoteHost = ''; // 远程主机地址(服务器返回)
|
||
late int remotePort = 0; // 远程主机端口(服务器返回)
|
||
final int localPort = 62289; // 本地端口
|
||
String localPublicHost = ''; // 本地公网ip地址
|
||
|
||
int heartbeatIntervalTime = 1; // 心跳包间隔时间(s)
|
||
|
||
Timer? _heartBeatTimer; // 心跳包定时器
|
||
bool _heartBeatTimerRunning = false; // 心跳包定时任务发送状态
|
||
|
||
String ToPeerId = ''; // 对端ID
|
||
String FromPeerId = ''; // 我的ID
|
||
|
||
// echo测试peer对端
|
||
final String echoPeerId = '3phX8Ng2cZHz5NtP8xAf6nYy2z1BYytoejgjoHrWMGhH';
|
||
|
||
bool isOnlineStartChartServer = false; // 星图是否上线成功
|
||
int reStartOnlineStartChartServerIntervalTime = 1; // 重新上线星图服务任务间隔(s)
|
||
Timer? reStartOnlineStartChartServerTimer; // 重新上线定时器
|
||
int talkPingIntervalTime = 1; // 发送通话保持消息间隔(s)
|
||
Timer? talkPingTimer; // 发送通话保持消息定时器
|
||
int talkExpectIntervalTime = 1; // 发送通话预期数据的消息间隔(s)
|
||
Timer? talkExpectTimer; // 发送通话预期消息定时器
|
||
int talkDataIntervalTime = 10; // 通话数据的消息间隔(ms)
|
||
Timer? talkDataTimer; // 发送通话数据消息定时器
|
||
|
||
final int _maxPayloadSize = 8 * 1024; // 分包大小
|
||
|
||
// 默认通话的期望数据格式
|
||
TalkExpect defaultTalkExpect = TalkExpect(
|
||
videoType: [TalkExpect_VideoTypeE.IMAGE],
|
||
audioType: [TalkExpect_AudioTypeE.G711],
|
||
);
|
||
|
||
// 默认通话数据
|
||
TalkData defaultTalkData = TalkData();
|
||
|
||
String relayPeerId = ''; // 中继peerId
|
||
|
||
// 获取 StartChartTalkStatus 的唯一实例
|
||
StartChartTalkStatus talkStatus = StartChartTalkStatus.instance;
|
||
|
||
// 星图服务初始化
|
||
Future<void> init() async {
|
||
if (isOnlineStartChartServer && _udpSocket != null) {
|
||
// 如果已经上线就不进行初始化
|
||
return;
|
||
}
|
||
// 节点注册
|
||
await _clientRegister();
|
||
// 中继查询
|
||
await _relayQuery();
|
||
// 初始化udp服务
|
||
await _onlineRelayService();
|
||
// 上报
|
||
await reportInformation();
|
||
}
|
||
|
||
/// 客户端注册
|
||
Future<void> _clientRegister() async {
|
||
final StarChartRegisterNodeEntity? registerNodeEntity =
|
||
await Storage.getStarChartRegisterNodeInfo();
|
||
if (registerNodeEntity != null && registerNodeEntity.peer?.id != null) {
|
||
_log(text: '获取到星图注册节点信息:$registerNodeEntity');
|
||
FromPeerId = registerNodeEntity.peer!.id ?? '';
|
||
} else {
|
||
_log(text: '开始注册客户端');
|
||
final StarChartRegisterNodeEntity requestStarChartRegisterNode =
|
||
await _requestStarChartRegisterNode();
|
||
await _saveStarChartRegisterNodeToStorage(requestStarChartRegisterNode);
|
||
FromPeerId = requestStarChartRegisterNode.peer!.id ?? '';
|
||
bindUserStarchart();
|
||
}
|
||
}
|
||
|
||
//绑定星图配置
|
||
Future<void> bindUserStarchart() async {
|
||
try {
|
||
final StarChartRegisterNodeEntity? registerNodeEntity =
|
||
await Storage.getStarChartRegisterNodeInfo();
|
||
|
||
final LoginEntity entity = await ApiRepository.to.bindUserStarchart(
|
||
starchartId: registerNodeEntity?.peer?.id ?? '',
|
||
starchartPeerPublicKey: registerNodeEntity?.peer?.publicKey ?? '',
|
||
starchartPeerPrivateKey: registerNodeEntity?.peer?.privateKey ?? '',
|
||
);
|
||
if (entity.errorCode!.codeIsSuccessful) {
|
||
AppLog.log('绑定成功');
|
||
} else {
|
||
AppLog.log('绑定失败');
|
||
}
|
||
} catch (e) {
|
||
AppLog.log('Error bindUserStarchart: $e');
|
||
}
|
||
}
|
||
|
||
// 中继查询
|
||
Future<void> _relayQuery() async {
|
||
final RelayInfoEntity relayInfoEntity =
|
||
await StartChartApi.to.relayQueryInfo();
|
||
_saveRelayInfoEntityToStorage(relayInfoEntity);
|
||
if (relayInfoEntity.client_addr != null) {
|
||
localPublicHost = relayInfoEntity.client_addr!;
|
||
}
|
||
if (relayInfoEntity.relay_list != null &&
|
||
relayInfoEntity.relay_list!.length > 0) {
|
||
for (int i = 0; i <= relayInfoEntity.relay_list!.length; i++) {
|
||
final data = relayInfoEntity.relay_list?[i];
|
||
if (data?.peerID != FromPeerId) {
|
||
final parseUdpUrl = _parseUdpUrl(data?.listenAddr ?? '');
|
||
remoteHost = parseUdpUrl['host'] ?? '';
|
||
remotePort = parseUdpUrl['port'] ?? '';
|
||
relayPeerId = data?.peerID ?? '';
|
||
ToPeerId = relayPeerId;
|
||
_log(text: '中继信息----》${relayInfoEntity}');
|
||
break;
|
||
}
|
||
}
|
||
} else {
|
||
_log(text: '未查询到中继信息----》');
|
||
}
|
||
}
|
||
|
||
void closeUdpSocket() {
|
||
if (_udpSocket != null) {
|
||
_udpSocket?.close();
|
||
}
|
||
}
|
||
|
||
// 初始化udp
|
||
Future<void> _onlineRelayService() async {
|
||
var addressIListenFrom = InternetAddress.anyIPv4;
|
||
RawDatagramSocket.bind(addressIListenFrom, localPort)
|
||
.then((RawDatagramSocket socket) {
|
||
_udpSocket = socket;
|
||
|
||
/// 广播功能
|
||
_udpSocket!.broadcastEnabled = true;
|
||
|
||
/// 设置数据接收回调
|
||
_onReceiveData(_udpSocket!);
|
||
}).catchError((error) {
|
||
_log(text: 'Failed to bind UDP socket: $error');
|
||
});
|
||
}
|
||
|
||
// 上报信息至发现服务
|
||
Future<void> reportInformation() async {
|
||
_log(text: '上报信息至发现服务');
|
||
// 构建参数
|
||
ReportInformationData data = await _makeReportInformationData();
|
||
final response = await StartChartApi.to.reportInformation(
|
||
reportInformationData: data,
|
||
);
|
||
if (response.statusCode == 200) {
|
||
talkStatus.setInitializationCompleted();
|
||
// 发送心跳消息
|
||
_sendHeartbeatMessage();
|
||
// 发送送上线消息
|
||
await reStartOnlineStartChartServer();
|
||
}
|
||
}
|
||
|
||
// 发送上线消息
|
||
Future<void> _sendOnlineMessage() async {
|
||
if (isOnlineStartChartServer) {
|
||
_log(text: '星图已上线,请勿重复发送上线消息');
|
||
return;
|
||
}
|
||
// 组装上线消息
|
||
final message = MessageCommand.goOnlineRelay(
|
||
FromPeerId: FromPeerId,
|
||
ToPeerId: ToPeerId,
|
||
MessageId: MessageCommand.getNextMessageId(ToPeerId, increment: true),
|
||
);
|
||
await _sendMessage(message: message);
|
||
}
|
||
|
||
// 发送对讲请求消息
|
||
Future<void> sendCallRequestMessage({required String ToPeerId}) async {
|
||
if (talkStatus.status == TalkStatus.duringCall) {
|
||
_log(text: '已经在通话中,请勿重复发送对讲请求');
|
||
return;
|
||
}
|
||
// 组装上线消息
|
||
final message = MessageCommand.talkRequestMessage(
|
||
FromPeerId: FromPeerId,
|
||
ToPeerId: ToPeerId,
|
||
MessageId: MessageCommand.getNextMessageId(ToPeerId, increment: true),
|
||
);
|
||
await _sendMessage(message: message);
|
||
}
|
||
|
||
// 发送对讲数据
|
||
Future<void> sendTalkDataMessage({required TalkData talkData}) async {
|
||
// 组装上线消息
|
||
final message = MessageCommand.talkDataMessage(
|
||
FromPeerId: FromPeerId,
|
||
ToPeerId: ToPeerId,
|
||
talkData: talkData,
|
||
MessageId: MessageCommand.getNextMessageId(ToPeerId, increment: true),
|
||
);
|
||
await _sendMessage(message: message);
|
||
}
|
||
|
||
// 发送心跳包消息
|
||
void _sendHeartbeatMessage() async {
|
||
if (_heartBeatTimerRunning) {
|
||
_log(text: '心跳已经开始了. 请勿重复发送心跳包消息');
|
||
return;
|
||
}
|
||
_heartBeatTimer ??= Timer.periodic(
|
||
Duration(
|
||
seconds: heartbeatIntervalTime,
|
||
),
|
||
(Timer timer) async {
|
||
final List<int> message = MessageCommand.heartbeatMessage(
|
||
FromPeerId: FromPeerId,
|
||
ToPeerId: relayPeerId,
|
||
MessageId:
|
||
MessageCommand.getNextMessageId(relayPeerId, increment: true),
|
||
);
|
||
await _sendMessage(message: message);
|
||
},
|
||
);
|
||
_heartBeatTimerRunning = true;
|
||
}
|
||
|
||
// 发送回声测试消息
|
||
void sendEchoMessage(
|
||
{required List<int> payload, required String toPeerId}) async {
|
||
// 计算需要分多少个包发送
|
||
final int totalPackets = (payload.length / _maxPayloadSize).ceil();
|
||
// 循环遍历
|
||
for (int i = 0; i < totalPackets; i++) {
|
||
int start = i * _maxPayloadSize;
|
||
int end = (i + 1) * _maxPayloadSize;
|
||
if (end > payload.length) {
|
||
end = payload.length;
|
||
}
|
||
// 取到分包数据
|
||
List<int> packet = payload.sublist(start, end);
|
||
|
||
// 分包数据不递增messageID
|
||
final messageId =
|
||
MessageCommand.getNextMessageId(toPeerId, increment: false);
|
||
// 组装分包数据
|
||
final message = MessageCommand.echoMessage(
|
||
ToPeerId: toPeerId,
|
||
FromPeerId: FromPeerId,
|
||
payload: packet,
|
||
SpTotal: totalPackets,
|
||
SpIndex: i + 1,
|
||
MessageId: messageId,
|
||
);
|
||
// 发送消息
|
||
await _sendMessage(message: message);
|
||
}
|
||
// 分包发送完了递增一下id
|
||
MessageCommand.getNextMessageId(toPeerId);
|
||
}
|
||
|
||
// 发送网关初始化消息
|
||
void sendGatewayResetMessage(
|
||
{required String ToPeerId, required int gatewayId}) async {
|
||
final message = MessageCommand.gatewayResetMessage(
|
||
ToPeerId: ToPeerId,
|
||
FromPeerId: FromPeerId,
|
||
gatewayId: gatewayId,
|
||
time: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||
MessageId: MessageCommand.getNextMessageId(ToPeerId, increment: true),
|
||
);
|
||
await _sendMessage(message: message);
|
||
}
|
||
|
||
// 发送同意接听消息
|
||
void sendTalkAcceptMessage() async {
|
||
final message = MessageCommand.talkAcceptMessage(
|
||
ToPeerId: ToPeerId,
|
||
FromPeerId: FromPeerId,
|
||
MessageId: MessageCommand.getNextMessageId(ToPeerId, increment: true),
|
||
);
|
||
await _sendMessage(message: message);
|
||
talkStatus.setWaitingAnswer();
|
||
}
|
||
|
||
// 发送拒绝接听消息
|
||
void sendTalkRejectMessage() async {
|
||
final message = MessageCommand.talkRejectMessage(
|
||
ToPeerId: ToPeerId,
|
||
FromPeerId: FromPeerId,
|
||
MessageId: MessageCommand.getNextMessageId(ToPeerId, increment: true),
|
||
);
|
||
await _sendMessage(message: message);
|
||
}
|
||
|
||
// 发送期望接受消息
|
||
void sendTalkExpectMessage({required TalkExpect talkExpect}) async {
|
||
final message = MessageCommand.talkExpectMessage(
|
||
ToPeerId: ToPeerId,
|
||
FromPeerId: FromPeerId,
|
||
talkExpect: talkExpect,
|
||
MessageId: MessageCommand.getNextMessageId(ToPeerId, increment: true),
|
||
);
|
||
await _sendMessage(message: message);
|
||
}
|
||
|
||
// 回复成功消息
|
||
void sendGenericRespSuccessMessage(
|
||
{required String ToPeerId,
|
||
required String FromPeerId,
|
||
required int PayloadType}) async {
|
||
final message = MessageCommand.genericRespSuccessMessage(
|
||
ToPeerId: ToPeerId,
|
||
FromPeerId: FromPeerId,
|
||
PayloadType: PayloadType,
|
||
MessageId: MessageCommand.getNextMessageId(ToPeerId, increment: false),
|
||
);
|
||
await _sendMessage(message: message);
|
||
}
|
||
|
||
// 回复失败消息
|
||
void sendGenericRespErrorMessage(
|
||
{required String ToPeerId,
|
||
required String FromPeerId,
|
||
required int PayloadType}) async {
|
||
final message = MessageCommand.genericRespErrorMessage(
|
||
ToPeerId: ToPeerId,
|
||
FromPeerId: FromPeerId,
|
||
PayloadType: PayloadType,
|
||
MessageId: MessageCommand.getNextMessageId(ToPeerId, increment: false),
|
||
);
|
||
await _sendMessage(message: message);
|
||
}
|
||
|
||
// 发送通话保持消息
|
||
Future<void> sendTalkPingMessage(
|
||
{required String ToPeerId, required String FromPeerId}) async {
|
||
final message = MessageCommand.talkPingMessage(
|
||
ToPeerId: ToPeerId,
|
||
FromPeerId: FromPeerId,
|
||
MessageId: MessageCommand.getNextMessageId(ToPeerId, increment: true),
|
||
);
|
||
await _sendMessage(message: message);
|
||
}
|
||
|
||
// 发送通话中挂断消息
|
||
Future<void> sendTalkHangupMessage() async {
|
||
final message = MessageCommand.talkHangupMessage(
|
||
ToPeerId: ToPeerId,
|
||
FromPeerId: FromPeerId,
|
||
MessageId: MessageCommand.getNextMessageId(ToPeerId, increment: true),
|
||
);
|
||
await _sendMessage(message: message);
|
||
}
|
||
|
||
// 重新上线
|
||
Future<void> reStartOnlineStartChartServer() async {
|
||
if (isOnlineStartChartServer) {
|
||
_log(text: '星图已上线,请勿重复发送上线消息');
|
||
return;
|
||
}
|
||
reStartOnlineStartChartServerTimer ??= Timer.periodic(
|
||
Duration(
|
||
seconds: reStartOnlineStartChartServerIntervalTime,
|
||
),
|
||
(Timer timer) async {
|
||
// 重新发送上线消息
|
||
await _sendOnlineMessage();
|
||
},
|
||
);
|
||
}
|
||
|
||
// 重新发送心跳
|
||
void reStartHeartBeat() {
|
||
stopHeartbeat();
|
||
_sendHeartbeatMessage();
|
||
}
|
||
|
||
// 停止定时发送心跳包
|
||
void stopHeartbeat() {
|
||
_heartBeatTimer?.cancel();
|
||
_heartBeatTimer = null; // 清除定时器引用
|
||
_heartBeatTimerRunning = false;
|
||
}
|
||
|
||
// 停止重新上线
|
||
void stopReStartOnlineStartChartServer() {
|
||
reStartOnlineStartChartServerTimer?.cancel();
|
||
reStartOnlineStartChartServerTimer = null; // 清除定时器引用
|
||
}
|
||
|
||
// 发送消息
|
||
Future<void> _sendMessage({required List<int> message}) async {
|
||
var result = await _udpSocket?.send(
|
||
message, InternetAddress(remoteHost), remotePort);
|
||
if (result != message.length) {
|
||
AppLog.log('❌Udp send data error----> $result ${message.length}');
|
||
// _udpSocket = null;
|
||
}
|
||
}
|
||
|
||
// 请求注册节点
|
||
Future<StarChartRegisterNodeEntity> _requestStarChartRegisterNode() async {
|
||
// 获取设备信息
|
||
final Map<String, String> deviceInfo = await _getDeviceInfo();
|
||
// 发送注册节点请求
|
||
final StarChartRegisterNodeEntity response =
|
||
await StartChartApi.to.starChartRegisterNode(
|
||
product: _productName,
|
||
model: '${deviceInfo['brand']}_${deviceInfo['model']}',
|
||
name: '${deviceInfo['id']}',
|
||
unique: deviceInfo['id'] ?? Uuid().v1(),
|
||
);
|
||
return response;
|
||
}
|
||
|
||
// 保存星图注册节点信息至缓存
|
||
Future<void> _saveStarChartRegisterNodeToStorage(
|
||
StarChartRegisterNodeEntity starChartRegisterNodeEntity) async {
|
||
if (starChartRegisterNodeEntity != null) {
|
||
await Storage.saveStarChartRegisterNodeInfo(starChartRegisterNodeEntity);
|
||
_log(text: '注册成功');
|
||
}
|
||
}
|
||
|
||
// 保存星图中继服务器信息至缓存
|
||
Future<void> _saveRelayInfoEntityToStorage(
|
||
RelayInfoEntity relayInfoEntity) async {
|
||
if (relayInfoEntity != null) {
|
||
await Storage.saveRelayInfo(relayInfoEntity);
|
||
}
|
||
}
|
||
|
||
// 构造上报信息数据参数
|
||
Future<ReportInformationData> _makeReportInformationData() async {
|
||
// 从缓存中获取中继信息
|
||
final RelayInfoEntity? relayInfoEntity = await Storage.getRelayInfo();
|
||
|
||
// 获取公钥
|
||
final publicKey = await getPublicKey();
|
||
// 获取私钥
|
||
final privateKey = await getPrivateKey();
|
||
// 生成签名
|
||
final sign = await _generateSign(
|
||
currentTimestamp: relayInfoEntity!.time ?? 0,
|
||
privateKeyHex: privateKey,
|
||
);
|
||
|
||
// 获取本机所有ip地址和中继返回的外网地址
|
||
final List<ListenAddrData> listenAddrDataList =
|
||
await _makeListenAddrDataList();
|
||
|
||
//
|
||
final RelayServiceData relayServiceData = RelayServiceData(
|
||
name: relayInfoEntity?.relay_list?[0].name ?? '',
|
||
listen_addr: relayInfoEntity?.relay_list?[0].listenAddr ?? '',
|
||
peers_max: relayInfoEntity?.relay_list?[0].peerMax ?? 0,
|
||
peers_current: relayInfoEntity?.relay_list?[0].peerCurrent ?? 0,
|
||
);
|
||
|
||
ReportInformationData data = ReportInformationData(
|
||
id: FromPeerId,
|
||
public_key: publicKey,
|
||
listen_addr: listenAddrDataList,
|
||
relay_service: relayServiceData,
|
||
time: relayInfoEntity.time ?? 0,
|
||
sign: sign,
|
||
);
|
||
|
||
return data;
|
||
}
|
||
|
||
// 解析对端数据
|
||
Future<void> analyzeInformationOtherEnd() async {
|
||
await StartChartApi.to.analyzeInformationOtherEnd(peerId: ToPeerId);
|
||
}
|
||
|
||
// 获取本机所有ip地址和中继返回的外网地址
|
||
Future<List<ListenAddrData>> _makeListenAddrDataList() async {
|
||
final List<ListenAddrData> listenAddrDataList = [];
|
||
final List<String> localIp = await _getAllIpAddresses();
|
||
// 从缓存中获取中继信息,取出返回的客户端ip地址
|
||
final RelayInfoEntity? relayInfoEntity = await Storage.getRelayInfo();
|
||
if (relayInfoEntity != null && relayInfoEntity.client_addr != null) {
|
||
listenAddrDataList.add(
|
||
ListenAddrData(
|
||
type: ListenAddrTypeConstant.relay,
|
||
address: relayInfoEntity.relay_list!.last!.listenAddr!,
|
||
),
|
||
);
|
||
}
|
||
localIp.forEach((element) {
|
||
listenAddrDataList.add(
|
||
ListenAddrData(
|
||
type: ListenAddrTypeConstant.local,
|
||
address: IpConstant.udpUrl + element + ':' + localPort.toString(),
|
||
),
|
||
);
|
||
});
|
||
return listenAddrDataList ?? [];
|
||
}
|
||
|
||
/// 获取本机所有 IP 地址
|
||
Future<List<String>> _getAllIpAddresses() async {
|
||
final List<String> ipAddresses = [];
|
||
try {
|
||
final List<NetworkInterface> interfaces = await NetworkInterface.list(
|
||
includeLoopback: true,
|
||
type: InternetAddressType.any,
|
||
);
|
||
|
||
for (final interface in interfaces) {
|
||
for (final address in interface.addresses) {
|
||
// 获取原始 IP 地址
|
||
String ipAddress = address.address;
|
||
// 移除 IPv6 地址中的接口标识符(如果有)
|
||
if (ipAddress.contains('%')) {
|
||
ipAddress = ipAddress.split('%').first;
|
||
}
|
||
// 确保 IP 地址不为空且不在排除列表中
|
||
if (ipAddress.isNotEmpty &&
|
||
!IpConstant.reportExcludeIp.contains(ipAddress)) {
|
||
ipAddresses.add(ipAddress);
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
_log(text: '❌--->获取本机IP时出现错误: $e');
|
||
}
|
||
return ipAddresses; // 注意:这里不需要 `?? []`,因为 `ipAddresses` 已经初始化为一个空列表。
|
||
}
|
||
|
||
void _log({required String text}) {
|
||
AppLog.log('$_productName=====${text}');
|
||
}
|
||
|
||
/// 获取设备信息
|
||
Future<Map<String, String>> _getDeviceInfo() async {
|
||
final Map<String, String> deviceInfo =
|
||
await DeviceInfoUtils.getDeviceInfo();
|
||
return deviceInfo;
|
||
}
|
||
|
||
/// 解析 UDP URL 并提取 IP 地址和端口号
|
||
Map<String, dynamic> _parseUdpUrl(String url) {
|
||
// 使用正则表达式匹配 IP 地址和端口号
|
||
final regex = RegExp(r'udp://(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}):(\d+)')
|
||
.firstMatch(url);
|
||
|
||
if (regex != null) {
|
||
final ip = regex.group(1);
|
||
final portStr = regex.group(2);
|
||
final port = int.tryParse(portStr ?? '');
|
||
|
||
if (ip != null && port != null) {
|
||
return {'host': ip, 'port': port};
|
||
}
|
||
}
|
||
throw FormatException('无法解析 URL 格式: $url');
|
||
}
|
||
|
||
String bytesToHex(List<int> bytes) {
|
||
return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join('');
|
||
}
|
||
|
||
// 生成签名sing
|
||
Future<String> _generateSign({
|
||
required int currentTimestamp,
|
||
required String privateKeyHex,
|
||
}) async {
|
||
String resultSign = '';
|
||
try {
|
||
// 1. 将 32 位时间戳以小端字节序编码为二进制数据
|
||
Uint8List signData = encodeTimestampToLittleEndianBytes(currentTimestamp);
|
||
|
||
// 2.将十六进制字符串转换为字节数组
|
||
List<int> privateKeyBytes = hexToBytes(privateKeyHex);
|
||
|
||
// 3.将私钥转换为 PEM 格式
|
||
final pemPrivateKey =
|
||
convertToPemPrivateKey(privateKeyBytes, isPKCS8: true);
|
||
// 4.签名
|
||
var result = await fastRsa.RSA
|
||
.signPKCS1v15Bytes(signData, fastRsa.Hash.SHA256, pemPrivateKey);
|
||
resultSign = hex.encode(result);
|
||
} catch (e) {
|
||
_log(text: '❌--->上报信息生成签名时出现错误: $e');
|
||
e.printError();
|
||
}
|
||
return resultSign ?? '';
|
||
}
|
||
|
||
// 将 32 位时间戳以小端字节序编码为二进制数据
|
||
Uint8List encodeTimestampToLittleEndianBytes(int timestamp) {
|
||
// 创建一个 4 字节的 ByteData 对象
|
||
ByteData byteData = ByteData(4);
|
||
|
||
// 将 32 位时间戳写入 ByteData,使用小端字节序
|
||
byteData.setUint32(0, timestamp, Endian.little);
|
||
|
||
// 将 ByteData 转换为 Uint8List
|
||
Uint8List bytes = byteData.buffer.asUint8List();
|
||
|
||
return bytes;
|
||
}
|
||
|
||
/// 转换私钥格式
|
||
String convertToPemPrivateKey(List<int> privateKeyBytes,
|
||
{bool isPKCS8 = true}) {
|
||
// 将字节数组转换为Base64编码的字符串
|
||
String base64PrivateKey = base64Encode(privateKeyBytes);
|
||
|
||
// 添加PEM格式的头尾标签
|
||
String pemHeader;
|
||
String pemFooter;
|
||
|
||
if (isPKCS8) {
|
||
pemHeader = "-----BEGIN PRIVATE KEY-----";
|
||
pemFooter = "-----END PRIVATE KEY-----";
|
||
} else {
|
||
pemHeader = "-----BEGIN RSA PRIVATE KEY-----";
|
||
pemFooter = "-----END RSA PRIVATE KEY-----";
|
||
}
|
||
|
||
// 将Base64字符串分行为每行64个字符
|
||
const lineLength = 64;
|
||
List<String> lines = []; // 用于存储每一行
|
||
|
||
for (int i = 0; i < base64PrivateKey.length; i += lineLength) {
|
||
int end = (i + lineLength < base64PrivateKey.length)
|
||
? i + lineLength
|
||
: base64PrivateKey.length;
|
||
lines.add(base64PrivateKey.substring(i, end));
|
||
}
|
||
|
||
// 组合成完整的PEM格式字符串
|
||
return "$pemHeader\n${lines.join('\n')}\n$pemFooter";
|
||
}
|
||
|
||
/// 自定义 PEM 格式的 RSA 私钥解析器
|
||
pc.RSAPrivateKey loadPrivateKey(String privateKeyHex) {
|
||
// 将十六进制字符串转换为字节数组
|
||
final uint8list = Uint8List.fromList(hexToBytes(privateKeyHex));
|
||
try {
|
||
// 使用 asn1lib 的 ASN1Parser 解析
|
||
final asn1Parser = asn1lib.ASN1Parser(uint8list);
|
||
final topLevelSeq = asn1Parser.nextObject() as asn1lib.ASN1Sequence;
|
||
|
||
final modulus = bytesToBigInt(
|
||
(topLevelSeq.elements[1] as asn1lib.ASN1Integer).valueBytes());
|
||
final privateExponent = bytesToBigInt(
|
||
(topLevelSeq.elements[3] as asn1lib.ASN1Integer).valueBytes());
|
||
final p = bytesToBigInt(
|
||
(topLevelSeq.elements[4] as asn1lib.ASN1Integer).valueBytes());
|
||
final q = bytesToBigInt(
|
||
(topLevelSeq.elements[5] as asn1lib.ASN1Integer).valueBytes());
|
||
|
||
return pc.RSAPrivateKey(modulus, privateExponent, p, q);
|
||
} catch (e) {
|
||
// 如果发生解码错误,打印错误信息
|
||
print("Error decoding private key: $e");
|
||
rethrow;
|
||
}
|
||
}
|
||
|
||
// 将十六进制字符串转换为字节数组
|
||
List<int> hexToBytes(String hex) {
|
||
return List<int>.generate(hex.length ~/ 2,
|
||
(i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16));
|
||
}
|
||
|
||
BigInt bytesToBigInt(Uint8List bytes) {
|
||
return BigInt.parse(
|
||
bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(),
|
||
radix: 16,
|
||
);
|
||
}
|
||
|
||
/// 获取公钥
|
||
Future<String> getPublicKey() async {
|
||
// 从缓存中获取星图注册节点信息
|
||
final StarChartRegisterNodeEntity? starChartRegisterNodeInfo =
|
||
await Storage.getStarChartRegisterNodeInfo();
|
||
return starChartRegisterNodeInfo?.peer?.publicKey ?? '';
|
||
}
|
||
|
||
/// 获取私钥
|
||
Future<String> getPrivateKey() async {
|
||
// 从缓存中获取星图注册节点信息
|
||
final StarChartRegisterNodeEntity? starChartRegisterNodeInfo =
|
||
await Storage.getStarChartRegisterNodeInfo();
|
||
return starChartRegisterNodeInfo?.peer?.privateKey ?? '';
|
||
}
|
||
|
||
// 接收返回的数据
|
||
void _onReceiveData(RawDatagramSocket socket) {
|
||
socket.listen((RawSocketEvent event) {
|
||
if (event == RawSocketEvent.read) {
|
||
Datagram? dg = socket.receive();
|
||
try {
|
||
if (dg?.data != null) {
|
||
final deserialize = ScpMessage.deserialize(dg!.data);
|
||
if (deserialize != null) {
|
||
// 处理返回数据
|
||
_handleUdpResultData(deserialize);
|
||
}
|
||
if (deserialize.PayloadType != PayloadTypeConstant.heartbeat) {
|
||
if (deserialize.Payload != null) {
|
||
_log(text: 'Udp收到结构体数据---》$deserialize');
|
||
}
|
||
|
||
// _log(text: 'text---》${utf8.decode(deserialize.Payload)}');
|
||
}
|
||
}
|
||
} catch (e, stackTrace) {
|
||
_log(text: '❌ Udp result data error ----> $e');
|
||
_log(text: '堆栈跟踪:\n$stackTrace');
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// 处理udp返回的数据
|
||
void _handleUdpResultData(ScpMessage scpMessage) {
|
||
final int payloadType = scpMessage.PayloadType ?? 0;
|
||
final int messageType = scpMessage.MessageType ?? 0;
|
||
try {
|
||
final ScpMessageHandler handler =
|
||
ScpMessageHandlerFactory.createHandler(payloadType);
|
||
if (messageType == MessageTypeConstant.Req) {
|
||
handler.handleReq(scpMessage);
|
||
} else if (messageType == MessageTypeConstant.Resp) {
|
||
handler.handleResp(scpMessage);
|
||
} else if (messageType == MessageTypeConstant.RealTimeData) {
|
||
handler.handleRealTimeData(scpMessage);
|
||
} else {
|
||
handler.handleInvalidReq(scpMessage);
|
||
}
|
||
} catch (e, stackTrace) {
|
||
_log(text: '❌ 处理udp返回数据时遇到错误---> $e\n,$stackTrace');
|
||
}
|
||
}
|
||
|
||
/// 通话保持定时器
|
||
void startTalkPingMessageTimer() {
|
||
talkPingTimer ??= Timer.periodic(
|
||
Duration(
|
||
seconds: talkPingIntervalTime,
|
||
),
|
||
(Timer timer) async {
|
||
await sendTalkPingMessage(
|
||
ToPeerId: ToPeerId,
|
||
FromPeerId: FromPeerId,
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
// 停止通话保持
|
||
void stopTalkPingMessageTimer() {
|
||
talkPingTimer?.cancel();
|
||
talkPingTimer = null; // 清除定时器引用
|
||
}
|
||
|
||
/// 通话期望数据定时器
|
||
void startTalkExpectTimer() {
|
||
talkExpectTimer ??= Timer.periodic(
|
||
Duration(
|
||
seconds: talkExpectIntervalTime,
|
||
),
|
||
(Timer timer) {
|
||
// 发送期望接受消息
|
||
sendTalkExpectMessage(
|
||
talkExpect: defaultTalkExpect,
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
/// 通话数据定时器
|
||
void startTalkDataTimer() async {
|
||
// 如果已经启动了就不运行
|
||
if (talkDataTimer != null) return;
|
||
// 读取 assets 文件
|
||
final ByteData data = await rootBundle.load('assets/talk.h264');
|
||
final List<int> byteData = data.buffer.asUint8List();
|
||
int current = 0;
|
||
int start = 0;
|
||
int end = 0;
|
||
final List<int> chunks = extractChunks(byteData);
|
||
talkDataTimer ??= Timer.periodic(
|
||
Duration(
|
||
milliseconds: talkDataIntervalTime,
|
||
),
|
||
(Timer timer) {
|
||
if (current >= chunks.length) {
|
||
print('数据已经发完');
|
||
start = 0;
|
||
end = 0;
|
||
current = 0;
|
||
timer.cancel();
|
||
return;
|
||
}
|
||
// 提取 NALU 边界并生成 chunks
|
||
end = chunks[current];
|
||
current++;
|
||
List<int> frameData = byteData.sublist(start, end);
|
||
if (frameData.length == 0) timer.cancel();
|
||
defaultTalkData = TalkData(
|
||
content: frameData,
|
||
contentType: TalkData_ContentTypeE.H264,
|
||
);
|
||
start = end;
|
||
// 发送童话数据
|
||
sendTalkDataMessage(talkData: defaultTalkData);
|
||
},
|
||
);
|
||
}
|
||
|
||
List<int> extractChunks(List<int> byteData) {
|
||
int i = 0;
|
||
int length = byteData.length;
|
||
int naluCount = 0;
|
||
int value;
|
||
int state = 0;
|
||
int lastIndex = 0;
|
||
List<int> result = [];
|
||
const minNaluPerChunk = 22; // 每个数据块包含的最小NALU数量
|
||
|
||
while (i < length) {
|
||
value = byteData[i++];
|
||
// finding 3 or 4-byte start codes (00 00 01 OR 00 00 00 01)
|
||
switch (state) {
|
||
case 0:
|
||
if (value == 0) {
|
||
state = 1;
|
||
}
|
||
break;
|
||
case 1:
|
||
if (value == 0) {
|
||
state = 2;
|
||
} else {
|
||
state = 0;
|
||
}
|
||
break;
|
||
case 2:
|
||
case 3:
|
||
if (value == 0) {
|
||
state = 3;
|
||
} else if (value == 1 && i < length) {
|
||
if (lastIndex > 0) {
|
||
naluCount++;
|
||
}
|
||
if (naluCount >= minNaluPerChunk) {
|
||
result.add(lastIndex - state - 1);
|
||
naluCount = 0;
|
||
}
|
||
state = 0;
|
||
lastIndex = i;
|
||
} else {
|
||
state = 0;
|
||
}
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (naluCount > 0) {
|
||
result.add(lastIndex);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
// 停止发送通话数据
|
||
void stopTalkDataTimer() {
|
||
talkDataTimer?.cancel();
|
||
talkDataTimer = null; // 清除定时器引用
|
||
}
|
||
|
||
// 停止发送通话期望数据
|
||
void stopTalkExpectMessageTimer() {
|
||
talkExpectTimer?.cancel();
|
||
talkExpectTimer = null; // 清除定时器引用
|
||
}
|
||
|
||
/// 修改预期接收到的数据
|
||
void changeTalkExpectDataType({required TalkExpect talkExpect}) {
|
||
defaultTalkExpect = talkExpect;
|
||
}
|
||
|
||
/// 销毁资源
|
||
void destruction() async {
|
||
isOnlineStartChartServer = false;
|
||
stopTalkExpectMessageTimer();
|
||
stopTalkPingMessageTimer();
|
||
stopHeartbeat();
|
||
stopReStartOnlineStartChartServer();
|
||
stopTalkDataTimer();
|
||
// await Storage.removerRelayInfo();
|
||
// await Storage.removerStarChartRegisterNodeInfo();
|
||
}
|
||
}
|