feat(controlled): 实现设备激活与安全认证流程
- 添加API客户端、加密存储和provision/token激活逻辑 - WebSocket改用Bearer令牌认证,移除REGISTER请求 - 设备ID改为服务端下发,支持令牌刷新和强制下线处理 - 新增deviceSecret加密存储和accessToken自动刷新 - 更新设备ID获取方式为出厂SN,添加安全存储依赖
This commit is contained in:
@@ -3,6 +3,7 @@ import 'dart:convert';
|
||||
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
|
||||
import '../api/api_client.dart';
|
||||
import '../models/signal_message.dart';
|
||||
import '../proto/control_message.pb.dart';
|
||||
import '../signaling/signaling_client.dart';
|
||||
@@ -12,8 +13,10 @@ import '../webrtc/webrtc_controller.dart';
|
||||
/// 对外暴露连接/断开/发送指令等高层接口(对应 Android 端 MainActivity 的流程)。
|
||||
class RemoteController {
|
||||
final String serverUrl;
|
||||
final String deviceId;
|
||||
final String targetDeviceId;
|
||||
final ApiClient apiClient;
|
||||
String? token;
|
||||
|
||||
String? authType;
|
||||
String? authValue;
|
||||
|
||||
@@ -21,7 +24,10 @@ class RemoteController {
|
||||
WebRtcController? _webRtc;
|
||||
Timer? _statsTimer;
|
||||
|
||||
/// 信令状态变化(如“正在连接…”、“已连接…”)。
|
||||
/// 本地设备ID(由服务端 REGISTER_SUCCESS 下发)。
|
||||
String? _myDeviceId;
|
||||
|
||||
/// 信令状态变化(如"正在连接…"、"已连接…)。
|
||||
void Function(String status)? onStatusChanged;
|
||||
|
||||
/// WebRTC 连接建立(可开始远程控制)。
|
||||
@@ -67,24 +73,39 @@ class RemoteController {
|
||||
/// 当前平台不支持原生硬解时回调。
|
||||
void Function()? onSelfCodecNotSupported;
|
||||
|
||||
/// 令牌失效(4001):用于触发刷新重连。
|
||||
void Function()? onTokenExpired;
|
||||
|
||||
/// 强制下线(4003):用于跳回登录。
|
||||
void Function()? onForceLogout;
|
||||
|
||||
RemoteController({
|
||||
required this.serverUrl,
|
||||
required this.deviceId,
|
||||
required this.targetDeviceId,
|
||||
required this.apiClient,
|
||||
this.token,
|
||||
this.authType,
|
||||
this.authValue,
|
||||
});
|
||||
|
||||
/// 发起连接:先连接信令服务器,成功后建立 WebRTC 并创建 Offer。
|
||||
void connect({String? authType, String? authValue}) {
|
||||
/// 发起连接:先确保 accessToken,成功后携带 Bearer 建立 WebSocket,
|
||||
/// 待 REGISTER_SUCCESS 拿到本机 deviceId 再建立 WebRTC 并创建 Offer。
|
||||
Future<void> connect({String? authType, String? authValue}) async {
|
||||
this.authType = authType;
|
||||
this.authValue = authValue;
|
||||
onStatusChanged?.call('状态: 正在连接信令服务器...');
|
||||
|
||||
_signaling = SignalingClient(serverUrl: serverUrl, deviceId: deviceId);
|
||||
try {
|
||||
token = await _ensureToken();
|
||||
} catch (e) {
|
||||
onStatusChanged?.call('状态: 认证失败 - $e');
|
||||
onConnectionFailed?.call(e.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
_signaling = SignalingClient(serverUrl: serverUrl, token: token);
|
||||
_signaling.onConnected = () {
|
||||
onStatusChanged?.call('状态: 已连接信令服务器,正在发起连接...');
|
||||
_initWebRtc();
|
||||
onStatusChanged?.call('状态: 已连接信令服务器,等待注册...');
|
||||
};
|
||||
_signaling.onMessage = _handleSignalMessage;
|
||||
_signaling.onDisconnected = () {
|
||||
@@ -95,10 +116,49 @@ class RemoteController {
|
||||
onStatusChanged?.call('状态: 连接错误 - $error');
|
||||
onConnectionFailed?.call(error);
|
||||
};
|
||||
_signaling.onTokenExpired = () async {
|
||||
try {
|
||||
await apiClient.refresh();
|
||||
token = apiClient.accessToken;
|
||||
_reconnect();
|
||||
} catch (e) {
|
||||
onStatusChanged?.call('状态: 令牌刷新失败 - $e');
|
||||
onTokenExpired?.call();
|
||||
}
|
||||
};
|
||||
_signaling.onForceLogout = () {
|
||||
onStatusChanged?.call('状态: 账号已在其他位置登录,已强制下线');
|
||||
onForceLogout?.call();
|
||||
};
|
||||
_signaling.connect();
|
||||
}
|
||||
|
||||
/// 确保 accessToken 有效:若已有则校验,失效则用 refreshToken 刷新。
|
||||
Future<String> _ensureToken() async {
|
||||
final existing = apiClient.accessToken;
|
||||
if (existing != null) {
|
||||
try {
|
||||
await apiClient.verify();
|
||||
return existing;
|
||||
} on ApiException catch (e) {
|
||||
if (e.httpCode != 401) return existing;
|
||||
}
|
||||
}
|
||||
final data = await apiClient.refresh();
|
||||
return data['accessToken'] as String;
|
||||
}
|
||||
|
||||
void _reconnect() {
|
||||
_signaling.disconnect();
|
||||
_signaling.connect();
|
||||
}
|
||||
|
||||
void _initWebRtc() {
|
||||
final deviceId = _myDeviceId;
|
||||
if (deviceId == null) {
|
||||
onStatusChanged?.call('状态: 未获取到本机设备ID,连接中止');
|
||||
return;
|
||||
}
|
||||
_webRtc = WebRtcController(
|
||||
signaling: _signaling,
|
||||
deviceId: deviceId,
|
||||
@@ -123,6 +183,7 @@ class RemoteController {
|
||||
_webRtc!.onFpsReport =
|
||||
(w, h, fps, list) => onFpsReport?.call(w, h, fps, list);
|
||||
_webRtc!.onSelfCodecNotSupported = () => onSelfCodecNotSupported?.call();
|
||||
onStatusChanged?.call('状态: 已注册 ($deviceId),正在发起连接...');
|
||||
_webRtc!.initialize().catchError((e) {
|
||||
onStatusChanged?.call('状态: 连接失败 - $e');
|
||||
onConnectionFailed?.call(e.toString());
|
||||
@@ -130,10 +191,20 @@ class RemoteController {
|
||||
}
|
||||
|
||||
void _handleSignalMessage(SignalMessage message) {
|
||||
switch (message.type?.toUpperCase()) {
|
||||
final type = message.type?.toUpperCase();
|
||||
if (type == 'REGISTER_SUCCESS') {
|
||||
// 服务端下发本机 deviceId,作为后续 OFFER 的 fromDeviceId。
|
||||
_myDeviceId = message.fromDeviceId;
|
||||
_initWebRtc();
|
||||
// 拉取可连接的被控端绑定列表(仅已绑定设备)。
|
||||
_loadBindings();
|
||||
// 尝试用服务端 TURN 凭证覆盖默认 ICE 配置。
|
||||
_loadTurnCredentials();
|
||||
return;
|
||||
}
|
||||
switch (type) {
|
||||
case 'ANSWER':
|
||||
final payload = jsonDecode(message.payload!) as Map<String, dynamic>;
|
||||
// 被控端已接受连接请求,进入 WebRTC 协商阶段。
|
||||
onStatusChanged?.call('状态: 被控端已接受连接,正在建立连接...');
|
||||
_webRtc?.handleAnswer(payload['sdp'] as String);
|
||||
break;
|
||||
@@ -167,6 +238,40 @@ class RemoteController {
|
||||
return payload;
|
||||
}
|
||||
|
||||
/// 拉取本机可连接的被控端绑定列表(仅已绑定设备),供 UI 提示。
|
||||
Future<void> _loadBindings() async {
|
||||
try {
|
||||
final data = await apiClient.bindings();
|
||||
final list = (data['bindings'] as List?) ?? [];
|
||||
if (list.isNotEmpty) {
|
||||
final uids = list.map((e) {
|
||||
if (e is Map) {
|
||||
return (e['deviceUid'] ?? e['deviceId'] ?? '').toString();
|
||||
}
|
||||
return e.toString();
|
||||
}).where((s) => s.isNotEmpty).join(', ');
|
||||
if (uids.isNotEmpty) {
|
||||
onStatusChanged?.call('已绑定设备: $uids');
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// 绑定列表拉取失败不影响主流程。
|
||||
}
|
||||
}
|
||||
|
||||
/// 拉取 TURN 短期凭证,覆盖默认 ICE 配置(服务端开启时)。
|
||||
Future<void> _loadTurnCredentials() async {
|
||||
final data = await apiClient.turnCredentials();
|
||||
if (data != null && data['iceServers'] is List) {
|
||||
final servers = (data['iceServers'] as List)
|
||||
.map((e) => Map<String, dynamic>.from(e as Map))
|
||||
.toList();
|
||||
if (servers.isNotEmpty) {
|
||||
WebRtcController.iceServersOverride = servers;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _startStats() {
|
||||
_statsTimer?.cancel();
|
||||
_statsTimer = Timer.periodic(const Duration(seconds: 1), (_) async {
|
||||
|
||||
Reference in New Issue
Block a user