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 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 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 stopControl() async { _statsTimer?.cancel(); _statsTimer = null; await _webRtc?.close(); _webRtc = null; } /// 确保 accessToken 有效:若已有则校验,失效则用 refreshToken 刷新。 Future _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; onStatusChanged?.call('状态: 被控端已接受连接,正在建立连接...'); _webRtc?.handleAnswer(payload['sdp'] as String); break; case 'ICE_CANDIDATE': final payload = jsonDecode(message.payload!) as Map; _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; final r = map['reason']; if (r is String && r.isNotEmpty) return r; } catch (_) {} return payload; } /// 拉取 TURN 短期凭证,覆盖默认 ICE 配置(服务端开启时)。 Future _loadTurnCredentials() async { final data = await authRepository.turnCredentials(); if (data != null && data['iceServers'] is List) { final servers = (data['iceServers'] as List) .map((e) => Map.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 sendStreamMode(int mode) => _webRtc?.sendStreamMode(mode) ?? Future.value(); /// 断开连接并释放资源(同时关闭 WebRTC 与信令)。 Future disconnect() async { await stopControl(); _signaling?.disconnect(); _signaling = null; _myDeviceId = null; } }