From bdde6ccd2ec1952f48952fc98f25763c7218f53f Mon Sep 17 00:00:00 2001 From: tongtongstudio Date: Wed, 22 Jul 2026 20:32:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=AF=86=E7=A0=81?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activity/main/MainActivity.java | 26 ++++++ .../service/ScreenCaptureService.java | 63 +++++++++++++- .../controlled/signaling/SignalMessage.java | 8 ++ .../ttstd/controlled/utils/AuthSettings.java | 60 ++++++++++++++ .../app/src/main/res/layout/activity_main.xml | 79 ++++++++++++++++++ .../com/ttstd/controller/MainActivity.java | 82 +++++++++++++++++-- .../controller/signaling/SignalMessage.java | 8 ++ .../ttstd/controller/webrtc/WebRtcClient.java | 9 +- .../src/main/res/layout/dialog_auth_input.xml | 44 ++++++++++ .../src/components/ConnectionPanel.vue | 26 +++++- .../src/services/SignalingClient.js | 9 +- .../src/services/WebRtcController.js | 4 +- .../src/store/controllerStore.js | 4 +- .../handler/SignalWebSocketHandler.java | 15 ++++ .../ttstd/signaling/model/SignalMessage.java | 8 ++ .../lib/controller/remote_controller.dart | 23 +++++- webrtc_controller_flutter/lib/main.dart | 74 ++++++++++++++++- .../lib/models/signal_message.dart | 14 ++++ .../lib/webrtc/webrtc_controller.dart | 10 ++- 19 files changed, 541 insertions(+), 25 deletions(-) create mode 100644 WebRTCControlled/app/src/main/java/com/ttstd/controlled/utils/AuthSettings.java create mode 100644 WebRTCController/app/src/main/res/layout/dialog_auth_input.xml diff --git a/WebRTCControlled/app/src/main/java/com/ttstd/controlled/activity/main/MainActivity.java b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/activity/main/MainActivity.java index bdca88e..d242bee 100644 --- a/WebRTCControlled/app/src/main/java/com/ttstd/controlled/activity/main/MainActivity.java +++ b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/activity/main/MainActivity.java @@ -32,6 +32,7 @@ import com.ttstd.controlled.base.BaseMvvmActivity; import com.ttstd.controlled.databinding.ActivityMainBinding; import com.ttstd.controlled.service.ScreenCaptureService; import com.ttstd.controlled.accessibility.KeyboardAccessibilityService; +import com.ttstd.controlled.utils.AuthSettings; import com.ttstd.controlled.utils.DeviceUtils; import com.ttstd.controlled.utils.SignatureUtils; import com.ttstd.controlled.webrtc.WebRtcClient; @@ -88,6 +89,31 @@ public class MainActivity extends BaseMvvmActivity startScreenSharing()); binding.btnStop.setOnClickListener(v -> stopScreenSharing()); + // 安全验证:显示动态验证码,提供重新生成与固定密码设置 + binding.tvDynamicCode.setText(AuthSettings.getDynamicCode(this)); + binding.btnRegenerateCode.setOnClickListener(v -> { + String newCode = AuthSettings.generateDynamicCode(); + AuthSettings.setDynamicCode(this, newCode); + binding.tvDynamicCode.setText(newCode); + binding.tvAuthHint.setText("动态验证码已重新生成"); + }); + binding.btnSavePassword.setOnClickListener(v -> { + String p1 = binding.etFixedPassword.getText().toString(); + String p2 = binding.etConfirmPassword.getText().toString(); + if (p1.isEmpty()) { + binding.tvAuthHint.setText("固定密码不能为空"); + return; + } + if (!p1.equals(p2)) { + binding.tvAuthHint.setText("两次输入的密码不一致"); + return; + } + AuthSettings.setFixedPassword(this, p1); + binding.etFixedPassword.setText(""); + binding.etConfirmPassword.setText(""); + binding.tvAuthHint.setText("固定密码已保存"); + }); + updateUI(false); } diff --git a/WebRTCControlled/app/src/main/java/com/ttstd/controlled/service/ScreenCaptureService.java b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/service/ScreenCaptureService.java index 3ed45a4..ff43ba3 100644 --- a/WebRTCControlled/app/src/main/java/com/ttstd/controlled/service/ScreenCaptureService.java +++ b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/service/ScreenCaptureService.java @@ -37,6 +37,7 @@ import com.ttstd.controlled.input.ShellInputUtils; import com.ttstd.controlled.input.SystemInputUtils; import com.ttstd.controlled.utils.SignatureUtils; import com.ttstd.controlled.signaling.SignalMessage; +import com.ttstd.controlled.utils.AuthSettings; import com.ttstd.controlled.signaling.WebSocketClient; import com.ttstd.controlled.webrtc.WebRtcClient; @@ -457,18 +458,78 @@ public class ScreenCaptureService extends Service { } String controllerId = message.getFromDeviceId(); - // 暂存请求,弹出确认对话框,交由用户决定是否接受。 + // 暂存请求信息 pendingControllerId = controllerId; pendingControllerName = (controllerName != null && !controllerName.isEmpty()) ? controllerName : controllerId; pendingOfferSdp = offerSdp; + // 鉴权:动态验证码(CODE) / 固定密码(PASSWORD)。 + // 服务器仅做转发,真实校验在此完成(密钥不离开被控端)。 + String authType = message.getAuthType(); + String authValue = message.getAuthValue(); + if (authType != null && !authType.trim().isEmpty()) { + String failReason = verifyAuth(authType.trim(), authValue); + if (failReason == null) { + // 校验通过:自动接受,直接建立连接(无需手动确认) + Log.i(TAG, "Auth passed (" + authType + "), auto-accepting " + controllerId); + mainHandler.post(this::acceptConnection); + } else { + Log.w(TAG, "Auth failed (" + authType + "): " + failReason + " from " + controllerId); + sendAuthRejected(failReason); + } + return; + } + + // 未携带鉴权信息:沿用原有手动确认弹窗 Intent dialogIntent = new Intent(this, ConnectionRequestActivity.class); dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); dialogIntent.putExtra(ConnectionRequestActivity.EXTRA_CONTROLLER_ID, controllerId); startActivity(dialogIntent); } + /** + * 校验控制端发来的鉴权信息。返回 null 表示通过,否则返回失败原因。 + * CODE: 与被控端当前动态验证码比对; + * PASSWORD: 与被控端固定密码比对。 + */ + private String verifyAuth(String authType, String authValue) { + if (authValue == null) authValue = ""; + if ("CODE".equalsIgnoreCase(authType)) { + String expected = AuthSettings.getDynamicCode(this); + if (expected == null || expected.isEmpty()) { + return "被控端未生成动态验证码"; + } + return expected.equals(authValue) ? null : "动态验证码错误"; + } else if ("PASSWORD".equalsIgnoreCase(authType)) { + String expected = AuthSettings.getFixedPassword(this); + if (expected == null || expected.isEmpty()) { + return "被控端未设置固定密码"; + } + return expected.equals(authValue) ? null : "固定密码错误"; + } + return "未知的鉴权类型: " + authType; + } + + /** 鉴权失败时,向控制端回送带原因的拒绝消息并清理待处理状态。 */ + private void sendAuthRejected(String reason) { + if (pendingControllerId == null || wsClient == null) { + return; + } + String controllerId = pendingControllerId; + clearPendingRequest(); + SignalMessage msg = new SignalMessage(); + msg.setType("CONNECTION_REJECTED"); + msg.setFromDeviceId(deviceId); + msg.setToDeviceId(controllerId); + msg.setDeviceType("CONTROLLED"); + JsonObject payload = new JsonObject(); + payload.addProperty("reason", reason); + msg.setPayload(payload.toString()); + wsClient.sendMessage(msg); + Log.i(TAG, "Sent CONNECTION_REJECTED(" + reason + ") to " + controllerId); + } + /** * 用户在弹窗中点击“接受”:创建 Answer 完成连接。 */ diff --git a/WebRTCControlled/app/src/main/java/com/ttstd/controlled/signaling/SignalMessage.java b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/signaling/SignalMessage.java index 7212e38..2ad32ab 100644 --- a/WebRTCControlled/app/src/main/java/com/ttstd/controlled/signaling/SignalMessage.java +++ b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/signaling/SignalMessage.java @@ -7,6 +7,8 @@ public class SignalMessage { private String toDeviceId; private String deviceType; private String payload; + private String authType; + private String authValue; public SignalMessage() {} @@ -31,4 +33,10 @@ public class SignalMessage { public String getPayload() { return payload; } public void setPayload(String payload) { this.payload = payload; } + + public String getAuthType() { return authType; } + public void setAuthType(String authType) { this.authType = authType; } + + public String getAuthValue() { return authValue; } + public void setAuthValue(String authValue) { this.authValue = authValue; } } diff --git a/WebRTCControlled/app/src/main/java/com/ttstd/controlled/utils/AuthSettings.java b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/utils/AuthSettings.java new file mode 100644 index 0000000..9ef75dd --- /dev/null +++ b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/utils/AuthSettings.java @@ -0,0 +1,60 @@ +package com.ttstd.controlled.utils; + +import android.content.Context; +import android.content.SharedPreferences; + +import java.security.SecureRandom; + +/** + * 被控端安全验证配置:动态验证码与固定密码。 + * 密钥仅保存在被控端本机,服务器只做转发,不接触任何密钥。 + */ +public class AuthSettings { + + private static final String PREF_NAME = "controlled_auth_prefs"; + private static final String KEY_DYNAMIC_CODE = "dynamic_code"; + private static final String KEY_FIXED_PASSWORD = "fixed_password"; + + private static final String CODE_CHARS = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + private static final int CODE_LENGTH = 6; + private static final SecureRandom RANDOM = new SecureRandom(); + + /** 生成一个 6 位、由大小写字母与数字组成的随机验证码。 */ + public static String generateDynamicCode() { + StringBuilder sb = new StringBuilder(CODE_LENGTH); + for (int i = 0; i < CODE_LENGTH; i++) { + sb.append(CODE_CHARS.charAt(RANDOM.nextInt(CODE_CHARS.length()))); + } + return sb.toString(); + } + + /** + * 获取当前动态验证码;若不存在则随机生成并持久化。 + */ + public static String getDynamicCode(Context context) { + SharedPreferences sp = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + String code = sp.getString(KEY_DYNAMIC_CODE, null); + if (code == null || code.isEmpty()) { + code = generateDynamicCode(); + sp.edit().putString(KEY_DYNAMIC_CODE, code).apply(); + } + return code; + } + + public static void setDynamicCode(Context context, String code) { + context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + .edit().putString(KEY_DYNAMIC_CODE, code == null ? "" : code).apply(); + } + + /** 获取固定密码(未设置时为空字符串)。 */ + public static String getFixedPassword(Context context) { + return context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + .getString(KEY_FIXED_PASSWORD, ""); + } + + public static void setFixedPassword(Context context, String password) { + context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + .edit().putString(KEY_FIXED_PASSWORD, password == null ? "" : password).apply(); + } +} diff --git a/WebRTCControlled/app/src/main/res/layout/activity_main.xml b/WebRTCControlled/app/src/main/res/layout/activity_main.xml index e736a90..9eee8e1 100644 --- a/WebRTCControlled/app/src/main/res/layout/activity_main.xml +++ b/WebRTCControlled/app/src/main/res/layout/activity_main.xml @@ -61,6 +61,85 @@ android:textSize="16sp" android:textStyle="bold" /> + + + + + + + + + + @@ -64,3 +83,8 @@ function onConnectDevice() { + + diff --git a/WebRTCControllerWeb/src/services/SignalingClient.js b/WebRTCControllerWeb/src/services/SignalingClient.js index f696fe6..db5a1ff 100644 --- a/WebRTCControllerWeb/src/services/SignalingClient.js +++ b/WebRTCControllerWeb/src/services/SignalingClient.js @@ -81,14 +81,17 @@ export class SignalingClient { this.send({ type: 'DEVICE_LIST', fromDeviceId: this.deviceId }); } - sendOffer(sdp, toDeviceId) { - this.send({ + sendOffer(sdp, toDeviceId, authType = null, authValue = null) { + const msg = { type: 'OFFER', fromDeviceId: this.deviceId, toDeviceId, deviceType: 'CONTROLLER', payload: JSON.stringify({ sdp }), - }); + }; + if (authType) msg.authType = authType; + if (authValue != null) msg.authValue = authValue; + this.send(msg); } sendIceCandidate(candidate, toDeviceId) { diff --git a/WebRTCControllerWeb/src/services/WebRtcController.js b/WebRTCControllerWeb/src/services/WebRtcController.js index 7f40236..7b4d1a2 100644 --- a/WebRTCControllerWeb/src/services/WebRtcController.js +++ b/WebRTCControllerWeb/src/services/WebRtcController.js @@ -35,7 +35,7 @@ export class WebRtcController { this._pendingCandidates = []; } - async createOffer() { + async createOffer(authType = null, authValue = null) { this.pc = new RTCPeerConnection({ iceServers: this.iceServers }); this.pc.onicecandidate = (e) => { @@ -88,7 +88,7 @@ export class WebRtcController { const offer = await this.pc.createOffer(); await this.pc.setLocalDescription(offer); - this.signaling.sendOffer(this.pc.localDescription.sdp, this.targetDeviceId); + this.signaling.sendOffer(this.pc.localDescription.sdp, this.targetDeviceId, authType, authValue); this._startStats(); } diff --git a/WebRTCControllerWeb/src/store/controllerStore.js b/WebRTCControllerWeb/src/store/controllerStore.js index f696da3..60e92fc 100644 --- a/WebRTCControllerWeb/src/store/controllerStore.js +++ b/WebRTCControllerWeb/src/store/controllerStore.js @@ -120,7 +120,7 @@ export function refreshDevices() { signaling && signaling.requestDeviceList(); } -export async function connectToDevice(targetId) { +export async function connectToDevice(targetId, authType = null, authValue = null) { if (!signaling || !store.signalingConnected) { store.error = '请先连接信令服务器'; return; } if (!targetId) { store.error = '请选择要控制的被控端设备'; return; } @@ -147,7 +147,7 @@ export async function connectToDevice(targetId) { // 被控端上报当前实际采集分辨率,同步到 store 供 UI 展示/匹配预设。 onResolutionReport: (res) => { store.currentResolution = res; }, }); - await webrtc.createOffer(); + await webrtc.createOffer(authType, authValue); } catch (e) { // 捕获 createOffer 阶段的所有异常,避免未处理的 Promise 拒绝导致浏览器中断脚本执行、 // 间接影响 WebSocket 信令连接的存活。 diff --git a/WebRTCSignalServer/src/main/java/com/ttstd/signaling/handler/SignalWebSocketHandler.java b/WebRTCSignalServer/src/main/java/com/ttstd/signaling/handler/SignalWebSocketHandler.java index 9faf05a..758119e 100644 --- a/WebRTCSignalServer/src/main/java/com/ttstd/signaling/handler/SignalWebSocketHandler.java +++ b/WebRTCSignalServer/src/main/java/com/ttstd/signaling/handler/SignalWebSocketHandler.java @@ -189,6 +189,21 @@ public class SignalWebSocketHandler extends TextWebSocketHandler { return; } + // 鉴权类型分类:动态验证码(CODE) / 固定密码(PASSWORD)。 + // 服务器只做分类与转发,真实校验在被控端完成(服务器不保存任何密钥)。 + String authType = message.getAuthType(); + if (authType != null && !authType.trim().isEmpty()) { + if ("CODE".equalsIgnoreCase(authType)) { + logger.info("连接请求 {} -> {} 使用【动态验证码】鉴权", fromDeviceId, toDeviceId); + } else if ("PASSWORD".equalsIgnoreCase(authType)) { + logger.info("连接请求 {} -> {} 使用【固定密码】鉴权", fromDeviceId, toDeviceId); + } else { + logger.warn("连接请求 {} -> {} 携带未知鉴权类型 authType={}", fromDeviceId, toDeviceId, authType); + } + } else { + logger.info("连接请求 {} -> {} 未携带鉴权(走被控端手动确认)", fromDeviceId, toDeviceId); + } + // 2. 去重:短时间内重复 OFFER 直接忽略,避免被控端反复弹窗 if (connectionRequestManager.isDuplicateOffer(fromDeviceId, toDeviceId)) { logger.info("Duplicate connection request {} -> {} ignored", fromDeviceId, toDeviceId); diff --git a/WebRTCSignalServer/src/main/java/com/ttstd/signaling/model/SignalMessage.java b/WebRTCSignalServer/src/main/java/com/ttstd/signaling/model/SignalMessage.java index 89fdf67..150cff8 100644 --- a/WebRTCSignalServer/src/main/java/com/ttstd/signaling/model/SignalMessage.java +++ b/WebRTCSignalServer/src/main/java/com/ttstd/signaling/model/SignalMessage.java @@ -10,6 +10,8 @@ public class SignalMessage { private String toDeviceId; // 目标设备ID private String deviceType; // CONTROLLER 或 CONTROLLED private String payload; // JSON格式的信令数据(SDP/ICE/控制指令) + private String authType; // 鉴权类型:CODE(动态验证码)/ PASSWORD(固定密码) + private String authValue; // 鉴权值:动态验证码或固定密码 public SignalMessage() {} @@ -27,4 +29,10 @@ public class SignalMessage { public String getPayload() { return payload; } public void setPayload(String payload) { this.payload = payload; } + + public String getAuthType() { return authType; } + public void setAuthType(String authType) { this.authType = authType; } + + public String getAuthValue() { return authValue; } + public void setAuthValue(String authValue) { this.authValue = authValue; } } diff --git a/webrtc_controller_flutter/lib/controller/remote_controller.dart b/webrtc_controller_flutter/lib/controller/remote_controller.dart index dbf481d..90955a4 100644 --- a/webrtc_controller_flutter/lib/controller/remote_controller.dart +++ b/webrtc_controller_flutter/lib/controller/remote_controller.dart @@ -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; + 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 { diff --git a/webrtc_controller_flutter/lib/main.dart b/webrtc_controller_flutter/lib/main.dart index 18a4ac3..0e67fe8 100644 --- a/webrtc_controller_flutter/lib/main.dart +++ b/webrtc_controller_flutter/lib/main.dart @@ -99,7 +99,73 @@ class _ControllerHomeState extends State { ); } - Future _connect() async { + Future _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( + context: context, + builder: (ctx) => StatefulBuilder( + builder: (ctx, setDialogState) => CupertinoAlertDialog( + title: const Text('连接鉴权'), + content: Column( + children: [ + const SizedBox(height: 12), + CupertinoSegmentedControl( + 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 _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 { serverUrl: serverUrl, deviceId: deviceId, targetDeviceId: target, + authType: authType, + authValue: authValue, ); _controller!.onStatusChanged = _setStatus; _controller!.onConnectionEstablished = () { @@ -142,7 +210,7 @@ class _ControllerHomeState extends State { }; _controller!.onStats = (stats) => setState(() => _stats = stats); - _controller!.connect(); + _controller!.connect(authType: authType, authValue: authValue); } void _onRendererUpdate() { @@ -241,7 +309,7 @@ class _ControllerHomeState extends State { SizedBox( width: double.infinity, child: CupertinoButton.filled( - onPressed: _connect, + onPressed: _showAuthDialog, child: const Text('连接被控设备'), ), ), diff --git a/webrtc_controller_flutter/lib/models/signal_message.dart b/webrtc_controller_flutter/lib/models/signal_message.dart index c17f4df..34021f3 100644 --- a/webrtc_controller_flutter/lib/models/signal_message.dart +++ b/webrtc_controller_flutter/lib/models/signal_message.dart @@ -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 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 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 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, ); } diff --git a/webrtc_controller_flutter/lib/webrtc/webrtc_controller.dart b/webrtc_controller_flutter/lib/webrtc/webrtc_controller.dart index 5d72b40..cb51618 100644 --- a/webrtc_controller_flutter/lib/webrtc/webrtc_controller.dart +++ b/webrtc_controller_flutter/lib/webrtc/webrtc_controller.dart @@ -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); }