Files
VibeCoding/webrtc_controller_flutter/lib/features/connection/data/remote_controller.dart
TongTongStudio e1de4c8170 feat(remote): 修复键盘输入映射并重构连接流程
- Android端注入按键时显式设置SOURCE_KEYBOARD,修复IME丢弃事件导致输入框无法输入文字的问题
- Flutter端新增Flutter逻辑键到Android keyCode的转换映射,避免功能键误触发
- 重构RemoteController,分离信令连接与远程控制逻辑,支持设备列表选择与配对码兑换
- AuthRepository接口新增getBindings、getOnlineDevices、redeemPairingCode方法
- DioAuthRepository实现401自动刷新token重试机制
- ConnectionSessionState扩展设备列表、在线状态、配对码等状态字段
2026-08-03 20:22:29 +08:00

332 lines
11 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}