167 lines
5.4 KiB
Dart
167 lines
5.4 KiB
Dart
import 'dart:async';
|
||
import 'dart:convert';
|
||
|
||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||
|
||
import '../models/signal_message.dart';
|
||
import '../proto/control_message.pb.dart';
|
||
import '../signaling/signaling_client.dart';
|
||
import '../webrtc/webrtc_controller.dart';
|
||
|
||
/// 控制端编排器:组合信令客户端与 WebRTC 控制器,
|
||
/// 对外暴露连接/断开/发送指令等高层接口(对应 Android 端 MainActivity 的流程)。
|
||
class RemoteController {
|
||
final String serverUrl;
|
||
final String deviceId;
|
||
final String targetDeviceId;
|
||
final String? authType;
|
||
final String? authValue;
|
||
|
||
late final SignalingClient _signaling;
|
||
WebRtcController? _webRtc;
|
||
Timer? _statsTimer;
|
||
|
||
/// 信令状态变化(如“正在连接…”、“已连接…”)。
|
||
void Function(String status)? onStatusChanged;
|
||
|
||
/// WebRTC 连接建立(可开始远程控制)。
|
||
void Function()? onConnectionEstablished;
|
||
|
||
/// 连接断开。
|
||
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;
|
||
|
||
RemoteController({
|
||
required this.serverUrl,
|
||
required this.deviceId,
|
||
required this.targetDeviceId,
|
||
this.authType,
|
||
this.authValue,
|
||
});
|
||
|
||
/// 发起连接:先连接信令服务器,成功后建立 WebRTC 并创建 Offer。
|
||
void connect({String? authType, String? authValue}) {
|
||
this.authType = authType;
|
||
this.authValue = authValue;
|
||
onStatusChanged?.call('状态: 正在连接信令服务器...');
|
||
|
||
_signaling = SignalingClient(serverUrl: serverUrl, deviceId: deviceId);
|
||
_signaling.onConnected = () {
|
||
onStatusChanged?.call('状态: 已连接信令服务器,正在发起连接...');
|
||
_initWebRtc();
|
||
};
|
||
_signaling.onMessage = _handleSignalMessage;
|
||
_signaling.onDisconnected = () {
|
||
onStatusChanged?.call('状态: 已断开连接');
|
||
onDisconnected?.call();
|
||
};
|
||
_signaling.onError = (error) {
|
||
onStatusChanged?.call('状态: 连接错误 - $error');
|
||
};
|
||
_signaling.connect();
|
||
}
|
||
|
||
void _initWebRtc() {
|
||
_webRtc = WebRtcController(
|
||
signaling: _signaling,
|
||
deviceId: deviceId,
|
||
targetDeviceId: targetDeviceId,
|
||
authType: authType,
|
||
authValue: authValue,
|
||
);
|
||
_webRtc!.onConnectionEstablished = () {
|
||
onConnectionEstablished?.call();
|
||
_startStats();
|
||
};
|
||
_webRtc!.onDisconnected = () {
|
||
onDisconnected?.call();
|
||
};
|
||
_webRtc!.onIceDisconnected = (message) => onIceDisconnected?.call(message);
|
||
_webRtc!.onRemoteStream = (renderer) => onRemoteStream?.call(renderer);
|
||
_webRtc!.initialize().catchError((e) {
|
||
onStatusChanged?.call('状态: 连接失败 - $e');
|
||
});
|
||
}
|
||
|
||
void _handleSignalMessage(SignalMessage message) {
|
||
switch (message.type?.toUpperCase()) {
|
||
case 'ANSWER':
|
||
final payload = jsonDecode(message.payload!) as Map<String, dynamic>;
|
||
// 被控端已接受连接请求,进入 WebRTC 协商阶段。
|
||
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;
|
||
}
|
||
|
||
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);
|
||
}
|
||
|
||
/// 断开连接并释放资源。
|
||
Future<void> disconnect() async {
|
||
_statsTimer?.cancel();
|
||
_statsTimer = null;
|
||
await _webRtc?.close();
|
||
_webRtc = null;
|
||
_signaling.disconnect();
|
||
}
|
||
}
|