feat: 增加密码连接

This commit is contained in:
2026-07-22 20:32:52 +08:00
parent ef786b1529
commit bdde6ccd2e
19 changed files with 541 additions and 25 deletions

View File

@@ -14,6 +14,8 @@ class RemoteController {
final String serverUrl;
final String deviceId;
final String targetDeviceId;
final String? authType;
final String? authValue;
late final SignalingClient _signaling;
WebRtcController? _webRtc;
@@ -47,10 +49,14 @@ class RemoteController {
required this.serverUrl,
required this.deviceId,
required this.targetDeviceId,
this.authType,
this.authValue,
});
/// 发起连接:先连接信令服务器,成功后建立 WebRTC 并创建 Offer。
void connect() {
void connect({String? authType, String? authValue}) {
this.authType = authType;
this.authValue = authValue;
onStatusChanged?.call('状态: 正在连接信令服务器...');
_signaling = SignalingClient(serverUrl: serverUrl, deviceId: deviceId);
@@ -74,6 +80,8 @@ class RemoteController {
signaling: _signaling,
deviceId: deviceId,
targetDeviceId: targetDeviceId,
authType: authType,
authValue: authValue,
);
_webRtc!.onConnectionEstablished = () {
onConnectionEstablished?.call();
@@ -109,13 +117,24 @@ class RemoteController {
case 'CONNECTION_REJECTED':
case 'REQUEST_ERROR':
case 'REQUEST_TIMEOUT':
final text = message.payload ?? '连接请求失败';
final text = _parseRejectReason(message.payload);
onStatusChanged?.call('状态: $text');
onConnectionRejected?.call(text);
break;
}
}
/// 解析拒绝原因:优先读取 JSON 中的 reason 字段,否则直接返回原文。
String _parseRejectReason(String? payload) {
if (payload == null || payload.isEmpty) return '连接请求失败';
try {
final map = jsonDecode(payload) as Map<String, dynamic>;
final r = map['reason'];
if (r is String && r.isNotEmpty) return r;
} catch (_) {}
return payload;
}
void _startStats() {
_statsTimer?.cancel();
_statsTimer = Timer.periodic(const Duration(seconds: 1), (_) async {

View File

@@ -99,7 +99,73 @@ class _ControllerHomeState extends State<ControllerHome> {
);
}
Future<void> _connect() async {
Future<void> _showAuthDialog() async {
final serverUrl = _serverUrlController.text.trim();
final deviceId = _deviceIdController.text.trim();
final target = _targetController.text.trim();
if (serverUrl.isEmpty || deviceId.isEmpty || target.isEmpty) {
_setStatus('请填写所有字段');
return;
}
String selectedType = 'CODE';
final valueController = TextEditingController();
await showCupertinoDialog<void>(
context: context,
builder: (ctx) => StatefulBuilder(
builder: (ctx, setDialogState) => CupertinoAlertDialog(
title: const Text('连接鉴权'),
content: Column(
children: [
const SizedBox(height: 12),
CupertinoSegmentedControl<String>(
children: const {
'CODE': Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Text('动态验证码'),
),
'PASSWORD': Padding(
padding: EdgeInsets.symmetric(horizontal: 8),
child: Text('固定密码'),
),
},
groupValue: selectedType,
onValueChanged: (v) => setDialogState(() => selectedType = v),
),
const SizedBox(height: 12),
CupertinoTextField(
controller: valueController,
placeholder: '请输入验证码或密码',
obscureText: true,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
),
],
),
actions: [
CupertinoDialogAction(
child: const Text('取消'),
onPressed: () => Navigator.of(ctx).pop(),
),
CupertinoDialogAction(
child: const Text('连接'),
onPressed: () {
final val = valueController.text.trim();
if (val.isEmpty) {
_showAlert('请输入验证码或密码');
return;
}
Navigator.of(ctx).pop();
_connect(authType: selectedType, authValue: val);
},
),
],
),
),
);
}
Future<void> _connect({required String authType, required String authValue}) async {
final serverUrl = _serverUrlController.text.trim();
final deviceId = _deviceIdController.text.trim();
final target = _targetController.text.trim();
@@ -115,6 +181,8 @@ class _ControllerHomeState extends State<ControllerHome> {
serverUrl: serverUrl,
deviceId: deviceId,
targetDeviceId: target,
authType: authType,
authValue: authValue,
);
_controller!.onStatusChanged = _setStatus;
_controller!.onConnectionEstablished = () {
@@ -142,7 +210,7 @@ class _ControllerHomeState extends State<ControllerHome> {
};
_controller!.onStats = (stats) => setState(() => _stats = stats);
_controller!.connect();
_controller!.connect(authType: authType, authValue: authValue);
}
void _onRendererUpdate() {
@@ -241,7 +309,7 @@ class _ControllerHomeState extends State<ControllerHome> {
SizedBox(
width: double.infinity,
child: CupertinoButton.filled(
onPressed: _connect,
onPressed: _showAuthDialog,
child: const Text('连接被控设备'),
),
),

View File

@@ -8,12 +8,16 @@ import 'dart:convert';
/// - [toDeviceId] 接收方设备 ID
/// - [deviceType] 设备类型CONTROLLER / CONTROLLED
/// - [payload] JSON 字符串形式的负载SDP / ICE 候选等)
/// - [authType] 鉴权类型CODE动态验证码/ PASSWORD固定密码
/// - [authValue] 鉴权值:动态验证码或固定密码
class SignalMessage {
final String? type;
final String? fromDeviceId;
final String? toDeviceId;
final String? deviceType;
final String? payload;
final String? authType;
final String? authValue;
const SignalMessage({
this.type,
@@ -21,6 +25,8 @@ class SignalMessage {
this.toDeviceId,
this.deviceType,
this.payload,
this.authType,
this.authValue,
});
factory SignalMessage.fromJson(Map<String, dynamic> json) => SignalMessage(
@@ -29,6 +35,8 @@ class SignalMessage {
toDeviceId: json['toDeviceId'] as String?,
deviceType: json['deviceType'] as String?,
payload: json['payload'] as String?,
authType: json['authType'] as String?,
authValue: json['authValue'] as String?,
);
Map<String, dynamic> toJson() => {
@@ -37,6 +45,8 @@ class SignalMessage {
if (toDeviceId != null) 'toDeviceId': toDeviceId,
if (deviceType != null) 'deviceType': deviceType,
if (payload != null) 'payload': payload,
if (authType != null) 'authType': authType,
if (authValue != null) 'authValue': authValue,
};
/// 便捷构造方法payload 为任意 Map会自动序列化为 JSON 字符串。
@@ -46,6 +56,8 @@ class SignalMessage {
required String toDeviceId,
required String deviceType,
required Map<String, dynamic> payload,
String? authType,
String? authValue,
}) {
return SignalMessage(
type: type,
@@ -53,6 +65,8 @@ class SignalMessage {
toDeviceId: toDeviceId,
deviceType: deviceType,
payload: jsonEncode(payload),
authType: authType,
authValue: authValue,
);
}

View File

@@ -21,6 +21,8 @@ class WebRtcController {
final SignalingClient signaling;
final String deviceId;
final String targetDeviceId;
final String? authType;
final String? authValue;
RTCPeerConnection? _pc;
RTCDataChannel? _dataChannel;
@@ -42,6 +44,8 @@ class WebRtcController {
required this.signaling,
required this.deviceId,
required this.targetDeviceId,
this.authType,
this.authValue,
});
/// 初始化渲染器、PeerConnection并创建 Offer。
@@ -89,7 +93,7 @@ class WebRtcController {
};
final offer = await _pc!.createOffer(constraints);
await _pc!.setLocalDescription(offer);
_sendOffer(offer.sdp!);
_sendOffer(offer.sdp!, authType: authType, authValue: authValue);
}
void _onTrack(RTCTrackEvent event) {
@@ -157,7 +161,7 @@ class WebRtcController {
}
}
void _sendOffer(String sdp) {
void _sendOffer(String sdp, {String? authType, String? authValue}) {
final payload = {'sdp': sdp};
final msg = SignalMessage.withPayload(
type: 'OFFER',
@@ -165,6 +169,8 @@ class WebRtcController {
toDeviceId: targetDeviceId,
deviceType: 'CONTROLLER',
payload: payload,
authType: authType,
authValue: authValue,
);
signaling.send(msg);
}