feat: 增加密码连接
This commit is contained in:
@@ -32,6 +32,7 @@ import com.ttstd.controlled.base.BaseMvvmActivity;
|
|||||||
import com.ttstd.controlled.databinding.ActivityMainBinding;
|
import com.ttstd.controlled.databinding.ActivityMainBinding;
|
||||||
import com.ttstd.controlled.service.ScreenCaptureService;
|
import com.ttstd.controlled.service.ScreenCaptureService;
|
||||||
import com.ttstd.controlled.accessibility.KeyboardAccessibilityService;
|
import com.ttstd.controlled.accessibility.KeyboardAccessibilityService;
|
||||||
|
import com.ttstd.controlled.utils.AuthSettings;
|
||||||
import com.ttstd.controlled.utils.DeviceUtils;
|
import com.ttstd.controlled.utils.DeviceUtils;
|
||||||
import com.ttstd.controlled.utils.SignatureUtils;
|
import com.ttstd.controlled.utils.SignatureUtils;
|
||||||
import com.ttstd.controlled.webrtc.WebRtcClient;
|
import com.ttstd.controlled.webrtc.WebRtcClient;
|
||||||
@@ -88,6 +89,31 @@ public class MainActivity extends BaseMvvmActivity<MainViewModel, ActivityMainBi
|
|||||||
binding.btnStart.setOnClickListener(v -> startScreenSharing());
|
binding.btnStart.setOnClickListener(v -> startScreenSharing());
|
||||||
binding.btnStop.setOnClickListener(v -> stopScreenSharing());
|
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);
|
updateUI(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import com.ttstd.controlled.input.ShellInputUtils;
|
|||||||
import com.ttstd.controlled.input.SystemInputUtils;
|
import com.ttstd.controlled.input.SystemInputUtils;
|
||||||
import com.ttstd.controlled.utils.SignatureUtils;
|
import com.ttstd.controlled.utils.SignatureUtils;
|
||||||
import com.ttstd.controlled.signaling.SignalMessage;
|
import com.ttstd.controlled.signaling.SignalMessage;
|
||||||
|
import com.ttstd.controlled.utils.AuthSettings;
|
||||||
import com.ttstd.controlled.signaling.WebSocketClient;
|
import com.ttstd.controlled.signaling.WebSocketClient;
|
||||||
import com.ttstd.controlled.webrtc.WebRtcClient;
|
import com.ttstd.controlled.webrtc.WebRtcClient;
|
||||||
|
|
||||||
@@ -457,18 +458,78 @@ public class ScreenCaptureService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String controllerId = message.getFromDeviceId();
|
String controllerId = message.getFromDeviceId();
|
||||||
// 暂存请求,弹出确认对话框,交由用户决定是否接受。
|
// 暂存请求信息
|
||||||
pendingControllerId = controllerId;
|
pendingControllerId = controllerId;
|
||||||
pendingControllerName = (controllerName != null && !controllerName.isEmpty())
|
pendingControllerName = (controllerName != null && !controllerName.isEmpty())
|
||||||
? controllerName : controllerId;
|
? controllerName : controllerId;
|
||||||
pendingOfferSdp = offerSdp;
|
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);
|
Intent dialogIntent = new Intent(this, ConnectionRequestActivity.class);
|
||||||
dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
dialogIntent.putExtra(ConnectionRequestActivity.EXTRA_CONTROLLER_ID, controllerId);
|
dialogIntent.putExtra(ConnectionRequestActivity.EXTRA_CONTROLLER_ID, controllerId);
|
||||||
startActivity(dialogIntent);
|
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 完成连接。
|
* 用户在弹窗中点击“接受”:创建 Answer 完成连接。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ public class SignalMessage {
|
|||||||
private String toDeviceId;
|
private String toDeviceId;
|
||||||
private String deviceType;
|
private String deviceType;
|
||||||
private String payload;
|
private String payload;
|
||||||
|
private String authType;
|
||||||
|
private String authValue;
|
||||||
|
|
||||||
public SignalMessage() {}
|
public SignalMessage() {}
|
||||||
|
|
||||||
@@ -31,4 +33,10 @@ public class SignalMessage {
|
|||||||
|
|
||||||
public String getPayload() { return payload; }
|
public String getPayload() { return payload; }
|
||||||
public void setPayload(String payload) { this.payload = 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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,6 +61,85 @@
|
|||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<!-- 安全验证:动态验证码 + 固定密码 -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:layout_marginBottom="6dp"
|
||||||
|
android:text="安全验证"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="动态验证码(连接时由控制端输入):"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_dynamic_code"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="------"
|
||||||
|
android:textSize="22sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:letterSpacing="0.1" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_regenerate_code"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="重新生成" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="固定密码(手动设置并确认):"
|
||||||
|
android:textSize="14sp" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/et_fixed_password"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:hint="输入固定密码"
|
||||||
|
android:inputType="textPassword" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/et_confirm_password"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:hint="再次输入固定密码"
|
||||||
|
android:inputType="textPassword" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_save_password"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:text="保存固定密码" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_auth_hint"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:textColor="#888888" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/btn_start"
|
android:id="@+id/btn_start"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import android.view.View;
|
|||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
import android.widget.ArrayAdapter;
|
import android.widget.ArrayAdapter;
|
||||||
import android.widget.Spinner;
|
import android.widget.Spinner;
|
||||||
|
import android.widget.RadioGroup;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -62,6 +64,8 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
private final Gson gson = new Gson();
|
private final Gson gson = new Gson();
|
||||||
private String myDeviceId;
|
private String myDeviceId;
|
||||||
private String targetDeviceId;
|
private String targetDeviceId;
|
||||||
|
private String pendingAuthType;
|
||||||
|
private String pendingAuthValue;
|
||||||
|
|
||||||
private final Handler statsHandler = new Handler(Looper.getMainLooper());
|
private final Handler statsHandler = new Handler(Looper.getMainLooper());
|
||||||
private final Runnable statsRunnable = new Runnable() {
|
private final Runnable statsRunnable = new Runnable() {
|
||||||
@@ -105,7 +109,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
myDeviceId = DeviceUtils.getSerialNumber(this);
|
myDeviceId = DeviceUtils.getSerialNumber(this);
|
||||||
etDeviceId.setText(myDeviceId);
|
etDeviceId.setText(myDeviceId);
|
||||||
|
|
||||||
btnConnect.setOnClickListener(v -> connectToControlled());
|
btnConnect.setOnClickListener(v -> showAuthDialog());
|
||||||
btnDisconnect.setOnClickListener(v -> disconnect());
|
btnDisconnect.setOnClickListener(v -> disconnect());
|
||||||
|
|
||||||
// 初始化 EGL 和远端视频渲染
|
// 初始化 EGL 和远端视频渲染
|
||||||
@@ -210,6 +214,56 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 弹出鉴权输入对话框:选择动态验证码或固定密码,输入后发起连接。 */
|
||||||
|
private void showAuthDialog() {
|
||||||
|
String serverUrl = etServerUrl.getText().toString().trim();
|
||||||
|
String target = etTargetDeviceId.getText().toString().trim();
|
||||||
|
String myId = etDeviceId.getText().toString().trim();
|
||||||
|
if (serverUrl.isEmpty() || target.isEmpty() || myId.isEmpty()) {
|
||||||
|
Toast.makeText(this, "请先填写服务器地址、设备ID和目标设备ID", Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
View view = getLayoutInflater().inflate(R.layout.dialog_auth_input, null);
|
||||||
|
RadioGroup rg = view.findViewById(R.id.rg_auth_type);
|
||||||
|
EditText et = view.findViewById(R.id.et_auth_value);
|
||||||
|
TextView tip = view.findViewById(R.id.tv_auth_tip);
|
||||||
|
|
||||||
|
rg.setOnCheckedChangeListener((group, checkedId) -> {
|
||||||
|
if (checkedId == R.id.rb_password) {
|
||||||
|
et.setHint("请输入固定密码");
|
||||||
|
tip.setText("固定密码为被控端设置的固定密码");
|
||||||
|
} else {
|
||||||
|
et.setHint("请输入动态验证码");
|
||||||
|
tip.setText("动态验证码为被控端界面显示的 6 位大小写字母与数字组合");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
AlertDialog dialog = new AlertDialog.Builder(this)
|
||||||
|
.setTitle("连接鉴权")
|
||||||
|
.setView(view)
|
||||||
|
.setNegativeButton("取消", null)
|
||||||
|
.setPositiveButton("连接", null)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
// 校验失败时不关闭对话框,引导用户重新输入
|
||||||
|
dialog.setOnShowListener(d -> dialog.getButton(AlertDialog.BUTTON_POSITIVE)
|
||||||
|
.setOnClickListener(v -> {
|
||||||
|
boolean isPassword = (rg.getCheckedRadioButtonId() == R.id.rb_password);
|
||||||
|
String authType = isPassword ? "PASSWORD" : "CODE";
|
||||||
|
String authValue = et.getText().toString().trim();
|
||||||
|
if (authValue.isEmpty()) {
|
||||||
|
Toast.makeText(MainActivity.this, "请输入验证码或密码", Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingAuthType = authType;
|
||||||
|
pendingAuthValue = authValue;
|
||||||
|
dialog.dismiss();
|
||||||
|
connectToControlled();
|
||||||
|
}));
|
||||||
|
dialog.show();
|
||||||
|
}
|
||||||
|
|
||||||
private void connectToControlled() {
|
private void connectToControlled() {
|
||||||
String serverUrl = etServerUrl.getText().toString().trim();
|
String serverUrl = etServerUrl.getText().toString().trim();
|
||||||
targetDeviceId = etTargetDeviceId.getText().toString().trim();
|
targetDeviceId = etTargetDeviceId.getText().toString().trim();
|
||||||
@@ -283,7 +337,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
webRtcClient.createOffer(targetDeviceId, remoteVideoView);
|
webRtcClient.createOffer(targetDeviceId, remoteVideoView, pendingAuthType, pendingAuthValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleSignalMessage(SignalMessage message) {
|
private void handleSignalMessage(SignalMessage message) {
|
||||||
@@ -330,17 +384,29 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
*/
|
*/
|
||||||
private void handleConnectionRejected(SignalMessage message) {
|
private void handleConnectionRejected(SignalMessage message) {
|
||||||
String targetId = message.getToDeviceId();
|
String targetId = message.getToDeviceId();
|
||||||
String payload = message.getPayload();
|
String reason = parseReason(message.getPayload());
|
||||||
String text = (payload != null && !payload.isEmpty())
|
String finalText = reason;
|
||||||
? payload
|
|
||||||
: "目标被控端拒绝了连接请求";
|
|
||||||
String finalText = text;
|
|
||||||
runOnUiThread(() -> {
|
runOnUiThread(() -> {
|
||||||
tvStatus.setText("状态: " + finalText);
|
tvStatus.setText("状态: " + finalText);
|
||||||
Toast.makeText(MainActivity.this, finalText, Toast.LENGTH_LONG).show();
|
Toast.makeText(MainActivity.this, finalText, Toast.LENGTH_LONG).show();
|
||||||
updateUI(false);
|
updateUI(false);
|
||||||
});
|
});
|
||||||
Log.w(TAG, "Connection request to " + targetId + " was rejected by controlled device");
|
Log.w(TAG, "Connection request to " + targetId + " was rejected: " + reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析拒绝原因:优先读取 JSON 中的 reason 字段,否则直接返回原文。 */
|
||||||
|
private String parseReason(String payload) {
|
||||||
|
if (payload == null || payload.isEmpty()) {
|
||||||
|
return "目标被控端拒绝了连接请求";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
JsonObject obj = gson.fromJson(payload, JsonObject.class);
|
||||||
|
if (obj != null && obj.has("reason") && !obj.get("reason").isJsonNull()) {
|
||||||
|
return obj.get("reason").getAsString();
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ public class SignalMessage {
|
|||||||
private String toDeviceId;
|
private String toDeviceId;
|
||||||
private String deviceType;
|
private String deviceType;
|
||||||
private String payload;
|
private String payload;
|
||||||
|
private String authType;
|
||||||
|
private String authValue;
|
||||||
|
|
||||||
public SignalMessage() {}
|
public SignalMessage() {}
|
||||||
|
|
||||||
@@ -31,4 +33,10 @@ public class SignalMessage {
|
|||||||
|
|
||||||
public String getPayload() { return payload; }
|
public String getPayload() { return payload; }
|
||||||
public void setPayload(String payload) { this.payload = 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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ public class WebRtcClient {
|
|||||||
private final WebSocketClient wsClient;
|
private final WebSocketClient wsClient;
|
||||||
private final String deviceId;
|
private final String deviceId;
|
||||||
private final Gson gson = new Gson();
|
private final Gson gson = new Gson();
|
||||||
|
private String pendingAuthType;
|
||||||
|
private String pendingAuthValue;
|
||||||
|
|
||||||
private PeerConnectionFactory peerConnectionFactory;
|
private PeerConnectionFactory peerConnectionFactory;
|
||||||
private PeerConnection peerConnection;
|
private PeerConnection peerConnection;
|
||||||
@@ -89,8 +91,11 @@ public class WebRtcClient {
|
|||||||
.createPeerConnectionFactory();
|
.createPeerConnectionFactory();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void createOffer(String controlledId, SurfaceViewRenderer remoteRenderer) {
|
public void createOffer(String controlledId, SurfaceViewRenderer remoteRenderer,
|
||||||
|
String authType, String authValue) {
|
||||||
this.currentControlledId = controlledId;
|
this.currentControlledId = controlledId;
|
||||||
|
this.pendingAuthType = authType;
|
||||||
|
this.pendingAuthValue = authValue;
|
||||||
|
|
||||||
List<PeerConnection.IceServer> iceServers = new ArrayList<>();
|
List<PeerConnection.IceServer> iceServers = new ArrayList<>();
|
||||||
// 添加 STUN 和 TURN 服务器
|
// 添加 STUN 和 TURN 服务器
|
||||||
@@ -309,6 +314,8 @@ public class WebRtcClient {
|
|||||||
msg.setFromDeviceId(deviceId);
|
msg.setFromDeviceId(deviceId);
|
||||||
msg.setToDeviceId(controlledId);
|
msg.setToDeviceId(controlledId);
|
||||||
msg.setDeviceType("CONTROLLER");
|
msg.setDeviceType("CONTROLLER");
|
||||||
|
if (pendingAuthType != null) msg.setAuthType(pendingAuthType);
|
||||||
|
if (pendingAuthValue != null) msg.setAuthValue(pendingAuthValue);
|
||||||
|
|
||||||
JsonObject payload = new JsonObject();
|
JsonObject payload = new JsonObject();
|
||||||
payload.addProperty("sdp", sdp);
|
payload.addProperty("sdp", sdp);
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="24dp">
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
android:id="@+id/rg_auth_type"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/rb_code"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:checked="true"
|
||||||
|
android:text="动态验证码" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/rb_password"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="固定密码" />
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/et_auth_value"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:hint="请输入动态验证码"
|
||||||
|
android:inputType="textVisiblePassword" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_auth_tip"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="动态验证码为被控端界面显示的 6 位大小写字母与数字组合"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="#888888" />
|
||||||
|
</LinearLayout>
|
||||||
@@ -3,9 +3,19 @@ import { ref } from 'vue';
|
|||||||
import { store, connectSignaling, disconnectSignaling, refreshDevices, connectToDevice, disconnectDevice } from '../store/controllerStore';
|
import { store, connectSignaling, disconnectSignaling, refreshDevices, connectToDevice, disconnectDevice } from '../store/controllerStore';
|
||||||
|
|
||||||
const selected = ref('');
|
const selected = ref('');
|
||||||
|
const authType = ref('CODE');
|
||||||
|
const authValue = ref('');
|
||||||
|
|
||||||
function onConnectDevice() {
|
function onConnectDevice() {
|
||||||
connectToDevice(selected.value);
|
if (!selected.value) {
|
||||||
|
store.error = '请选择要连接的设备';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!authValue.value.trim()) {
|
||||||
|
store.error = '请输入验证码或密码';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
connectToDevice(selected.value, authType.value, authValue.value.trim());
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -51,6 +61,15 @@ function onConnectDevice() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="section-title" style="margin-top:14px">连接鉴权</div>
|
||||||
|
<div class="auth-row">
|
||||||
|
<label class="auth-radio"><input type="radio" value="CODE" v-model="authType" /> 动态验证码</label>
|
||||||
|
<label class="auth-radio"><input type="radio" value="PASSWORD" v-model="authType" /> 固定密码</label>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<input class="input" v-model="authValue" :placeholder="authType === 'PASSWORD' ? '请输入固定密码' : '请输入动态验证码'" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<button class="btn" @click="onConnectDevice" :disabled="!selected || store.rtcConnected">发起远程控制</button>
|
<button class="btn" @click="onConnectDevice" :disabled="!selected || store.rtcConnected">发起远程控制</button>
|
||||||
<button class="btn danger" style="margin-top:12px" v-if="store.rtcConnected" @click="disconnectDevice">结束控制</button>
|
<button class="btn danger" style="margin-top:12px" v-if="store.rtcConnected" @click="disconnectDevice">结束控制</button>
|
||||||
|
|
||||||
@@ -64,3 +83,8 @@ function onConnectDevice() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.auth-row { display: flex; gap: 16px; margin-bottom: 8px; align-items: center; }
|
||||||
|
.auth-radio { display: inline-flex; align-items: center; gap: 4px; }
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -81,14 +81,17 @@ export class SignalingClient {
|
|||||||
this.send({ type: 'DEVICE_LIST', fromDeviceId: this.deviceId });
|
this.send({ type: 'DEVICE_LIST', fromDeviceId: this.deviceId });
|
||||||
}
|
}
|
||||||
|
|
||||||
sendOffer(sdp, toDeviceId) {
|
sendOffer(sdp, toDeviceId, authType = null, authValue = null) {
|
||||||
this.send({
|
const msg = {
|
||||||
type: 'OFFER',
|
type: 'OFFER',
|
||||||
fromDeviceId: this.deviceId,
|
fromDeviceId: this.deviceId,
|
||||||
toDeviceId,
|
toDeviceId,
|
||||||
deviceType: 'CONTROLLER',
|
deviceType: 'CONTROLLER',
|
||||||
payload: JSON.stringify({ sdp }),
|
payload: JSON.stringify({ sdp }),
|
||||||
});
|
};
|
||||||
|
if (authType) msg.authType = authType;
|
||||||
|
if (authValue != null) msg.authValue = authValue;
|
||||||
|
this.send(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendIceCandidate(candidate, toDeviceId) {
|
sendIceCandidate(candidate, toDeviceId) {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export class WebRtcController {
|
|||||||
this._pendingCandidates = [];
|
this._pendingCandidates = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOffer() {
|
async createOffer(authType = null, authValue = null) {
|
||||||
this.pc = new RTCPeerConnection({ iceServers: this.iceServers });
|
this.pc = new RTCPeerConnection({ iceServers: this.iceServers });
|
||||||
|
|
||||||
this.pc.onicecandidate = (e) => {
|
this.pc.onicecandidate = (e) => {
|
||||||
@@ -88,7 +88,7 @@ export class WebRtcController {
|
|||||||
|
|
||||||
const offer = await this.pc.createOffer();
|
const offer = await this.pc.createOffer();
|
||||||
await this.pc.setLocalDescription(offer);
|
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();
|
this._startStats();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ export function refreshDevices() {
|
|||||||
signaling && signaling.requestDeviceList();
|
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 (!signaling || !store.signalingConnected) { store.error = '请先连接信令服务器'; return; }
|
||||||
if (!targetId) { store.error = '请选择要控制的被控端设备'; return; }
|
if (!targetId) { store.error = '请选择要控制的被控端设备'; return; }
|
||||||
|
|
||||||
@@ -147,7 +147,7 @@ export async function connectToDevice(targetId) {
|
|||||||
// 被控端上报当前实际采集分辨率,同步到 store 供 UI 展示/匹配预设。
|
// 被控端上报当前实际采集分辨率,同步到 store 供 UI 展示/匹配预设。
|
||||||
onResolutionReport: (res) => { store.currentResolution = res; },
|
onResolutionReport: (res) => { store.currentResolution = res; },
|
||||||
});
|
});
|
||||||
await webrtc.createOffer();
|
await webrtc.createOffer(authType, authValue);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 捕获 createOffer 阶段的所有异常,避免未处理的 Promise 拒绝导致浏览器中断脚本执行、
|
// 捕获 createOffer 阶段的所有异常,避免未处理的 Promise 拒绝导致浏览器中断脚本执行、
|
||||||
// 间接影响 WebSocket 信令连接的存活。
|
// 间接影响 WebSocket 信令连接的存活。
|
||||||
|
|||||||
@@ -189,6 +189,21 @@ public class SignalWebSocketHandler extends TextWebSocketHandler {
|
|||||||
return;
|
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 直接忽略,避免被控端反复弹窗
|
// 2. 去重:短时间内重复 OFFER 直接忽略,避免被控端反复弹窗
|
||||||
if (connectionRequestManager.isDuplicateOffer(fromDeviceId, toDeviceId)) {
|
if (connectionRequestManager.isDuplicateOffer(fromDeviceId, toDeviceId)) {
|
||||||
logger.info("Duplicate connection request {} -> {} ignored", fromDeviceId, toDeviceId);
|
logger.info("Duplicate connection request {} -> {} ignored", fromDeviceId, toDeviceId);
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ public class SignalMessage {
|
|||||||
private String toDeviceId; // 目标设备ID
|
private String toDeviceId; // 目标设备ID
|
||||||
private String deviceType; // CONTROLLER 或 CONTROLLED
|
private String deviceType; // CONTROLLER 或 CONTROLLED
|
||||||
private String payload; // JSON格式的信令数据(SDP/ICE/控制指令)
|
private String payload; // JSON格式的信令数据(SDP/ICE/控制指令)
|
||||||
|
private String authType; // 鉴权类型:CODE(动态验证码)/ PASSWORD(固定密码)
|
||||||
|
private String authValue; // 鉴权值:动态验证码或固定密码
|
||||||
|
|
||||||
public SignalMessage() {}
|
public SignalMessage() {}
|
||||||
|
|
||||||
@@ -27,4 +29,10 @@ public class SignalMessage {
|
|||||||
|
|
||||||
public String getPayload() { return payload; }
|
public String getPayload() { return payload; }
|
||||||
public void setPayload(String payload) { this.payload = 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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ class RemoteController {
|
|||||||
final String serverUrl;
|
final String serverUrl;
|
||||||
final String deviceId;
|
final String deviceId;
|
||||||
final String targetDeviceId;
|
final String targetDeviceId;
|
||||||
|
final String? authType;
|
||||||
|
final String? authValue;
|
||||||
|
|
||||||
late final SignalingClient _signaling;
|
late final SignalingClient _signaling;
|
||||||
WebRtcController? _webRtc;
|
WebRtcController? _webRtc;
|
||||||
@@ -47,10 +49,14 @@ class RemoteController {
|
|||||||
required this.serverUrl,
|
required this.serverUrl,
|
||||||
required this.deviceId,
|
required this.deviceId,
|
||||||
required this.targetDeviceId,
|
required this.targetDeviceId,
|
||||||
|
this.authType,
|
||||||
|
this.authValue,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 发起连接:先连接信令服务器,成功后建立 WebRTC 并创建 Offer。
|
/// 发起连接:先连接信令服务器,成功后建立 WebRTC 并创建 Offer。
|
||||||
void connect() {
|
void connect({String? authType, String? authValue}) {
|
||||||
|
this.authType = authType;
|
||||||
|
this.authValue = authValue;
|
||||||
onStatusChanged?.call('状态: 正在连接信令服务器...');
|
onStatusChanged?.call('状态: 正在连接信令服务器...');
|
||||||
|
|
||||||
_signaling = SignalingClient(serverUrl: serverUrl, deviceId: deviceId);
|
_signaling = SignalingClient(serverUrl: serverUrl, deviceId: deviceId);
|
||||||
@@ -74,6 +80,8 @@ class RemoteController {
|
|||||||
signaling: _signaling,
|
signaling: _signaling,
|
||||||
deviceId: deviceId,
|
deviceId: deviceId,
|
||||||
targetDeviceId: targetDeviceId,
|
targetDeviceId: targetDeviceId,
|
||||||
|
authType: authType,
|
||||||
|
authValue: authValue,
|
||||||
);
|
);
|
||||||
_webRtc!.onConnectionEstablished = () {
|
_webRtc!.onConnectionEstablished = () {
|
||||||
onConnectionEstablished?.call();
|
onConnectionEstablished?.call();
|
||||||
@@ -109,13 +117,24 @@ class RemoteController {
|
|||||||
case 'CONNECTION_REJECTED':
|
case 'CONNECTION_REJECTED':
|
||||||
case 'REQUEST_ERROR':
|
case 'REQUEST_ERROR':
|
||||||
case 'REQUEST_TIMEOUT':
|
case 'REQUEST_TIMEOUT':
|
||||||
final text = message.payload ?? '连接请求失败';
|
final text = _parseRejectReason(message.payload);
|
||||||
onStatusChanged?.call('状态: $text');
|
onStatusChanged?.call('状态: $text');
|
||||||
onConnectionRejected?.call(text);
|
onConnectionRejected?.call(text);
|
||||||
break;
|
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() {
|
void _startStats() {
|
||||||
_statsTimer?.cancel();
|
_statsTimer?.cancel();
|
||||||
_statsTimer = Timer.periodic(const Duration(seconds: 1), (_) async {
|
_statsTimer = Timer.periodic(const Duration(seconds: 1), (_) async {
|
||||||
|
|||||||
@@ -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 serverUrl = _serverUrlController.text.trim();
|
||||||
final deviceId = _deviceIdController.text.trim();
|
final deviceId = _deviceIdController.text.trim();
|
||||||
final target = _targetController.text.trim();
|
final target = _targetController.text.trim();
|
||||||
@@ -115,6 +181,8 @@ class _ControllerHomeState extends State<ControllerHome> {
|
|||||||
serverUrl: serverUrl,
|
serverUrl: serverUrl,
|
||||||
deviceId: deviceId,
|
deviceId: deviceId,
|
||||||
targetDeviceId: target,
|
targetDeviceId: target,
|
||||||
|
authType: authType,
|
||||||
|
authValue: authValue,
|
||||||
);
|
);
|
||||||
_controller!.onStatusChanged = _setStatus;
|
_controller!.onStatusChanged = _setStatus;
|
||||||
_controller!.onConnectionEstablished = () {
|
_controller!.onConnectionEstablished = () {
|
||||||
@@ -142,7 +210,7 @@ class _ControllerHomeState extends State<ControllerHome> {
|
|||||||
};
|
};
|
||||||
_controller!.onStats = (stats) => setState(() => _stats = stats);
|
_controller!.onStats = (stats) => setState(() => _stats = stats);
|
||||||
|
|
||||||
_controller!.connect();
|
_controller!.connect(authType: authType, authValue: authValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onRendererUpdate() {
|
void _onRendererUpdate() {
|
||||||
@@ -241,7 +309,7 @@ class _ControllerHomeState extends State<ControllerHome> {
|
|||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: CupertinoButton.filled(
|
child: CupertinoButton.filled(
|
||||||
onPressed: _connect,
|
onPressed: _showAuthDialog,
|
||||||
child: const Text('连接被控设备'),
|
child: const Text('连接被控设备'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -8,12 +8,16 @@ import 'dart:convert';
|
|||||||
/// - [toDeviceId] 接收方设备 ID
|
/// - [toDeviceId] 接收方设备 ID
|
||||||
/// - [deviceType] 设备类型:CONTROLLER / CONTROLLED
|
/// - [deviceType] 设备类型:CONTROLLER / CONTROLLED
|
||||||
/// - [payload] JSON 字符串形式的负载(SDP / ICE 候选等)
|
/// - [payload] JSON 字符串形式的负载(SDP / ICE 候选等)
|
||||||
|
/// - [authType] 鉴权类型:CODE(动态验证码)/ PASSWORD(固定密码)
|
||||||
|
/// - [authValue] 鉴权值:动态验证码或固定密码
|
||||||
class SignalMessage {
|
class SignalMessage {
|
||||||
final String? type;
|
final String? type;
|
||||||
final String? fromDeviceId;
|
final String? fromDeviceId;
|
||||||
final String? toDeviceId;
|
final String? toDeviceId;
|
||||||
final String? deviceType;
|
final String? deviceType;
|
||||||
final String? payload;
|
final String? payload;
|
||||||
|
final String? authType;
|
||||||
|
final String? authValue;
|
||||||
|
|
||||||
const SignalMessage({
|
const SignalMessage({
|
||||||
this.type,
|
this.type,
|
||||||
@@ -21,6 +25,8 @@ class SignalMessage {
|
|||||||
this.toDeviceId,
|
this.toDeviceId,
|
||||||
this.deviceType,
|
this.deviceType,
|
||||||
this.payload,
|
this.payload,
|
||||||
|
this.authType,
|
||||||
|
this.authValue,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory SignalMessage.fromJson(Map<String, dynamic> json) => SignalMessage(
|
factory SignalMessage.fromJson(Map<String, dynamic> json) => SignalMessage(
|
||||||
@@ -29,6 +35,8 @@ class SignalMessage {
|
|||||||
toDeviceId: json['toDeviceId'] as String?,
|
toDeviceId: json['toDeviceId'] as String?,
|
||||||
deviceType: json['deviceType'] as String?,
|
deviceType: json['deviceType'] as String?,
|
||||||
payload: json['payload'] as String?,
|
payload: json['payload'] as String?,
|
||||||
|
authType: json['authType'] as String?,
|
||||||
|
authValue: json['authValue'] as String?,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
@@ -37,6 +45,8 @@ class SignalMessage {
|
|||||||
if (toDeviceId != null) 'toDeviceId': toDeviceId,
|
if (toDeviceId != null) 'toDeviceId': toDeviceId,
|
||||||
if (deviceType != null) 'deviceType': deviceType,
|
if (deviceType != null) 'deviceType': deviceType,
|
||||||
if (payload != null) 'payload': payload,
|
if (payload != null) 'payload': payload,
|
||||||
|
if (authType != null) 'authType': authType,
|
||||||
|
if (authValue != null) 'authValue': authValue,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// 便捷构造方法:payload 为任意 Map,会自动序列化为 JSON 字符串。
|
/// 便捷构造方法:payload 为任意 Map,会自动序列化为 JSON 字符串。
|
||||||
@@ -46,6 +56,8 @@ class SignalMessage {
|
|||||||
required String toDeviceId,
|
required String toDeviceId,
|
||||||
required String deviceType,
|
required String deviceType,
|
||||||
required Map<String, dynamic> payload,
|
required Map<String, dynamic> payload,
|
||||||
|
String? authType,
|
||||||
|
String? authValue,
|
||||||
}) {
|
}) {
|
||||||
return SignalMessage(
|
return SignalMessage(
|
||||||
type: type,
|
type: type,
|
||||||
@@ -53,6 +65,8 @@ class SignalMessage {
|
|||||||
toDeviceId: toDeviceId,
|
toDeviceId: toDeviceId,
|
||||||
deviceType: deviceType,
|
deviceType: deviceType,
|
||||||
payload: jsonEncode(payload),
|
payload: jsonEncode(payload),
|
||||||
|
authType: authType,
|
||||||
|
authValue: authValue,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ class WebRtcController {
|
|||||||
final SignalingClient signaling;
|
final SignalingClient signaling;
|
||||||
final String deviceId;
|
final String deviceId;
|
||||||
final String targetDeviceId;
|
final String targetDeviceId;
|
||||||
|
final String? authType;
|
||||||
|
final String? authValue;
|
||||||
|
|
||||||
RTCPeerConnection? _pc;
|
RTCPeerConnection? _pc;
|
||||||
RTCDataChannel? _dataChannel;
|
RTCDataChannel? _dataChannel;
|
||||||
@@ -42,6 +44,8 @@ class WebRtcController {
|
|||||||
required this.signaling,
|
required this.signaling,
|
||||||
required this.deviceId,
|
required this.deviceId,
|
||||||
required this.targetDeviceId,
|
required this.targetDeviceId,
|
||||||
|
this.authType,
|
||||||
|
this.authValue,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 初始化渲染器、PeerConnection,并创建 Offer。
|
/// 初始化渲染器、PeerConnection,并创建 Offer。
|
||||||
@@ -89,7 +93,7 @@ class WebRtcController {
|
|||||||
};
|
};
|
||||||
final offer = await _pc!.createOffer(constraints);
|
final offer = await _pc!.createOffer(constraints);
|
||||||
await _pc!.setLocalDescription(offer);
|
await _pc!.setLocalDescription(offer);
|
||||||
_sendOffer(offer.sdp!);
|
_sendOffer(offer.sdp!, authType: authType, authValue: authValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onTrack(RTCTrackEvent event) {
|
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 payload = {'sdp': sdp};
|
||||||
final msg = SignalMessage.withPayload(
|
final msg = SignalMessage.withPayload(
|
||||||
type: 'OFFER',
|
type: 'OFFER',
|
||||||
@@ -165,6 +169,8 @@ class WebRtcController {
|
|||||||
toDeviceId: targetDeviceId,
|
toDeviceId: targetDeviceId,
|
||||||
deviceType: 'CONTROLLER',
|
deviceType: 'CONTROLLER',
|
||||||
payload: payload,
|
payload: payload,
|
||||||
|
authType: authType,
|
||||||
|
authValue: authValue,
|
||||||
);
|
);
|
||||||
signaling.send(msg);
|
signaling.send(msg);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user