- Android端注入按键时显式设置SOURCE_KEYBOARD,修复IME丢弃事件导致输入框无法输入文字的问题 - Flutter端新增Flutter逻辑键到Android keyCode的转换映射,避免功能键误触发 - 重构RemoteController,分离信令连接与远程控制逻辑,支持设备列表选择与配对码兑换 - AuthRepository接口新增getBindings、getOnlineDevices、redeemPairingCode方法 - DioAuthRepository实现401自动刷新token重试机制 - ConnectionSessionState扩展设备列表、在线状态、配对码等状态字段
332 lines
11 KiB
Dart
332 lines
11 KiB
Dart
import 'dart:async';
|
||
import 'dart:convert';
|
||
|
||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||
|
||
import '../../../core/network/api_exception.dart';
|
||
import '../../../core/proto/control_message.pb.dart';
|
||
import '../../auth/domain/auth_repository.dart';
|
||
import '../domain/signal_message.dart';
|
||
import 'signaling_client.dart';
|
||
import 'webrtc_controller.dart';
|
||
|
||
/// 控制端编排器:组合信令客户端与 WebRTC 控制器,
|
||
/// 对外暴露连接/断开/发送指令等高层接口(对应 Android 端 MainActivity 的流程)。
|
||
class RemoteController {
|
||
final String serverUrl;
|
||
final AuthRepository authRepository;
|
||
String? token;
|
||
|
||
/// 目标被控端设备ID,在发起远程控制时指定。
|
||
String? targetDeviceId;
|
||
|
||
String? authType;
|
||
String? authValue;
|
||
|
||
SignalingClient? _signaling;
|
||
WebRtcController? _webRtc;
|
||
Timer? _statsTimer;
|
||
|
||
/// 本地设备ID(由服务端 REGISTER_SUCCESS 下发)。
|
||
String? _myDeviceId;
|
||
|
||
/// 信令是否已注册成功(拿到本机 deviceId)。
|
||
bool get registered => _myDeviceId != null;
|
||
|
||
/// 本机设备ID(未注册时为 null)。
|
||
String? get myDeviceId => _myDeviceId;
|
||
|
||
/// 是否已发起远程控制(WebRTC 会话存在)。
|
||
bool get controlling => _webRtc != null;
|
||
|
||
/// 信令状态变化(如"正在连接…"、"已连接…)。
|
||
void Function(String status)? onStatusChanged;
|
||
|
||
/// WebRTC 连接建立(可开始远程控制)。
|
||
void Function()? onConnectionEstablished;
|
||
|
||
/// 连接失败。
|
||
void Function(String error)? onConnectionFailed;
|
||
|
||
/// 连接断开。
|
||
void Function()? onDisconnected;
|
||
|
||
/// ICE 连接断开(用于提示用户并返回连接设置)。
|
||
void Function(String message)? onIceDisconnected;
|
||
|
||
/// 目标被控端不在线(服务器回送 TARGET_OFFLINE,用于提示用户并复位 UI)。
|
||
void Function(String message)? onTargetOffline;
|
||
|
||
/// 被控端拒绝了连接请求(用于提示用户并复位 UI)。
|
||
void Function(String message)? onConnectionRejected;
|
||
|
||
/// 远端视频渲染器就绪。
|
||
void Function(RTCVideoRenderer renderer)? onRemoteStream;
|
||
|
||
/// 统计信息刷新(每秒一次)。
|
||
void Function(String stats)? onStats;
|
||
|
||
/// 自编码解码纹理已就绪(textureId >= 0)。
|
||
void Function(int textureId)? onSelfCodecReady;
|
||
|
||
/// 自编码解码器已释放(连接断开时)。
|
||
void Function()? onSelfCodecLost;
|
||
|
||
/// 被控端上报当前生效的串流模式。
|
||
void Function(int mode)? onStreamModeReport;
|
||
|
||
/// 被控端/解码器上报分辨率。
|
||
void Function(int width, int height)? onResolutionReported;
|
||
|
||
/// 被控端上报当前采集帧率与支持的帧率档位。
|
||
void Function(int width, int height, int fps, List<int> supportedFps)?
|
||
onFpsReport;
|
||
|
||
/// 当前平台不支持原生硬解时回调。
|
||
void Function()? onSelfCodecNotSupported;
|
||
|
||
/// 令牌失效(4001):用于触发刷新重连。
|
||
void Function()? onTokenExpired;
|
||
|
||
/// 强制下线(4003):用于跳回登录。
|
||
void Function()? onForceLogout;
|
||
|
||
/// 信令注册成功(下发本机 deviceId),此时可发起远程控制。
|
||
void Function(String myDeviceId)? onRegistered;
|
||
|
||
RemoteController({
|
||
required this.serverUrl,
|
||
required this.authRepository,
|
||
this.targetDeviceId,
|
||
this.token,
|
||
this.authType,
|
||
this.authValue,
|
||
});
|
||
|
||
/// 仅建立信令连接:确保 accessToken 后携带 Bearer 建立 WebSocket,
|
||
/// 待 REGISTER_SUCCESS 拿到本机 deviceId 即完成,**不会**发起远程控制。
|
||
///
|
||
/// 发起远程控制请在注册成功后调用 [startControl]。
|
||
Future<void> connectSignaling() async {
|
||
onStatusChanged?.call('状态: 正在连接信令服务器...');
|
||
|
||
try {
|
||
token = await _ensureToken();
|
||
} catch (e) {
|
||
onStatusChanged?.call('状态: 认证失败 - $e');
|
||
onConnectionFailed?.call(e.toString());
|
||
return;
|
||
}
|
||
|
||
final signaling = SignalingClient(serverUrl: serverUrl, token: token);
|
||
_signaling = signaling;
|
||
signaling.onConnected = () {
|
||
onStatusChanged?.call('状态: 已连接信令服务器,等待注册...');
|
||
};
|
||
signaling.onMessage = _handleSignalMessage;
|
||
signaling.onDisconnected = () {
|
||
_myDeviceId = null;
|
||
onStatusChanged?.call('状态: 已断开连接');
|
||
onDisconnected?.call();
|
||
};
|
||
signaling.onError = (error) {
|
||
onStatusChanged?.call('状态: 连接错误 - $error');
|
||
onConnectionFailed?.call(error);
|
||
};
|
||
signaling.onTokenExpired = () async {
|
||
try {
|
||
await authRepository.refresh();
|
||
token = authRepository.accessToken;
|
||
_reconnect();
|
||
} catch (e) {
|
||
onStatusChanged?.call('状态: 令牌刷新失败 - $e');
|
||
onTokenExpired?.call();
|
||
}
|
||
};
|
||
signaling.onForceLogout = () {
|
||
onStatusChanged?.call('状态: 账号已在其他位置登录,已强制下线');
|
||
onForceLogout?.call();
|
||
};
|
||
signaling.connect();
|
||
}
|
||
|
||
/// 发起远程控制:在信令注册成功后调用,建立 WebRTC 并向目标设备创建 Offer。
|
||
///
|
||
/// [target] 目标被控端设备ID;[authType]/[authValue] 为被控端鉴权方式。
|
||
void startControl({
|
||
required String target,
|
||
String? authType,
|
||
String? authValue,
|
||
}) {
|
||
if (!registered) {
|
||
onConnectionFailed?.call('信令未就绪,请稍后重试');
|
||
return;
|
||
}
|
||
if (_webRtc != null) return;
|
||
targetDeviceId = target;
|
||
this.authType = authType;
|
||
this.authValue = authValue;
|
||
_initWebRtc();
|
||
}
|
||
|
||
/// 停止远程控制但保持信令连接(便于重新选择设备发起连接)。
|
||
Future<void> stopControl() async {
|
||
_statsTimer?.cancel();
|
||
_statsTimer = null;
|
||
await _webRtc?.close();
|
||
_webRtc = null;
|
||
}
|
||
|
||
/// 确保 accessToken 有效:若已有则校验,失效则用 refreshToken 刷新。
|
||
Future<String> _ensureToken() async {
|
||
final existing = authRepository.accessToken;
|
||
if (existing != null) {
|
||
try {
|
||
await authRepository.verify();
|
||
return existing;
|
||
} on ApiException catch (e) {
|
||
if (e.httpCode != 401) return existing;
|
||
}
|
||
}
|
||
final data = await authRepository.refresh();
|
||
return data['accessToken'] as String;
|
||
}
|
||
|
||
void _reconnect() {
|
||
_signaling?.disconnect();
|
||
_signaling?.connect();
|
||
}
|
||
|
||
void _initWebRtc() {
|
||
final deviceId = _myDeviceId;
|
||
final signaling = _signaling;
|
||
final target = targetDeviceId;
|
||
if (deviceId == null || signaling == null || target == null) {
|
||
onStatusChanged?.call('状态: 未获取到本机设备ID,连接中止');
|
||
return;
|
||
}
|
||
_webRtc = WebRtcController(
|
||
signaling: signaling,
|
||
deviceId: deviceId,
|
||
targetDeviceId: target,
|
||
authType: authType,
|
||
authValue: authValue,
|
||
);
|
||
_webRtc!.onConnectionEstablished = () {
|
||
onConnectionEstablished?.call();
|
||
_startStats();
|
||
};
|
||
_webRtc!.onConnectionFailed = (error) => onConnectionFailed?.call(error);
|
||
_webRtc!.onDisconnected = () {
|
||
onDisconnected?.call();
|
||
};
|
||
_webRtc!.onIceDisconnected = (message) => onIceDisconnected?.call(message);
|
||
_webRtc!.onRemoteStream = (renderer) => onRemoteStream?.call(renderer);
|
||
_webRtc!.onSelfCodecReady = (id) => onSelfCodecReady?.call(id);
|
||
_webRtc!.onSelfCodecLost = () => onSelfCodecLost?.call();
|
||
_webRtc!.onStreamModeReport = (mode) => onStreamModeReport?.call(mode);
|
||
_webRtc!.onResolutionReported = (w, h) => onResolutionReported?.call(w, h);
|
||
_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());
|
||
});
|
||
}
|
||
|
||
void _handleSignalMessage(SignalMessage message) {
|
||
final type = message.type?.toUpperCase();
|
||
if (type == 'REGISTER_SUCCESS') {
|
||
// 服务端下发本机 deviceId,作为后续 OFFER 的 fromDeviceId。
|
||
final deviceId = message.fromDeviceId;
|
||
_myDeviceId = deviceId;
|
||
onStatusChanged?.call('状态: 已连接信令服务器 ($deviceId)');
|
||
if (deviceId != null) onRegistered?.call(deviceId);
|
||
// 尝试用服务端 TURN 凭证覆盖默认 ICE 配置。
|
||
_loadTurnCredentials();
|
||
return;
|
||
}
|
||
switch (type) {
|
||
case 'ANSWER':
|
||
final payload = jsonDecode(message.payload!) as Map<String, dynamic>;
|
||
onStatusChanged?.call('状态: 被控端已接受连接,正在建立连接...');
|
||
_webRtc?.handleAnswer(payload['sdp'] as String);
|
||
break;
|
||
case 'ICE_CANDIDATE':
|
||
final payload = jsonDecode(message.payload!) as Map<String, dynamic>;
|
||
_webRtc?.handleIceCandidate(payload);
|
||
break;
|
||
case 'TARGET_OFFLINE':
|
||
final text = message.payload ?? '目标被控端不在线,请确认设备已开启并连接服务器';
|
||
onStatusChanged?.call('状态: $text');
|
||
onTargetOffline?.call(text);
|
||
break;
|
||
case 'CONNECTION_REJECTED':
|
||
case 'REQUEST_ERROR':
|
||
case 'REQUEST_TIMEOUT':
|
||
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;
|
||
}
|
||
|
||
/// 拉取 TURN 短期凭证,覆盖默认 ICE 配置(服务端开启时)。
|
||
Future<void> _loadTurnCredentials() async {
|
||
final data = await authRepository.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 {
|
||
final text = await _webRtc?.getStatsText();
|
||
if (text != null && text.isNotEmpty) {
|
||
onStats?.call(text);
|
||
}
|
||
});
|
||
}
|
||
|
||
/// 发送控制指令(protobuf 二进制)。
|
||
void sendControlCommand(ControlMessage command) {
|
||
_webRtc?.sendControlCommand(command);
|
||
}
|
||
|
||
/// 请求被控端切换屏幕采集分辨率。
|
||
void sendResolutionChange(int width, int height, int fps) {
|
||
_webRtc?.sendResolutionChange(width, height, fps);
|
||
}
|
||
|
||
/// 请求被控端切换屏幕串流模式(0=WebRTC 全托管 / 1=自编码)。
|
||
Future<void> sendStreamMode(int mode) =>
|
||
_webRtc?.sendStreamMode(mode) ?? Future.value();
|
||
|
||
/// 断开连接并释放资源(同时关闭 WebRTC 与信令)。
|
||
Future<void> disconnect() async {
|
||
await stopControl();
|
||
_signaling?.disconnect();
|
||
_signaling = null;
|
||
_myDeviceId = null;
|
||
}
|
||
}
|