docs(webrtc_controller_flutter): 更新项目文档以反映重构后的架构

AGENTS.md 与 README.md 同步更新:根据实际代码结构重写目录树、技术栈、架构分层及编码规范,移除旧版内联示例并补充新的开发约定与代码生成命令。
This commit is contained in:
2026-08-03 16:12:44 +08:00
parent 1918e5738e
commit d5e66a1777
51 changed files with 4711 additions and 1458 deletions

View File

@@ -0,0 +1,308 @@
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 String targetDeviceId;
final AuthRepository authRepository;
String? token;
String? authType;
String? authValue;
late final SignalingClient _signaling;
WebRtcController? _webRtc;
Timer? _statsTimer;
/// 本地设备ID由服务端 REGISTER_SUCCESS 下发)。
String? _myDeviceId;
/// 信令状态变化(如"正在连接…"、"已连接…)。
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;
RemoteController({
required this.serverUrl,
required this.targetDeviceId,
required this.authRepository,
this.token,
this.authType,
this.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('状态: 正在连接信令服务器...');
try {
token = await _ensureToken();
} catch (e) {
onStatusChanged?.call('状态: 认证失败 - $e');
onConnectionFailed?.call(e.toString());
return;
}
_signaling = SignalingClient(serverUrl: serverUrl, token: token);
_signaling.onConnected = () {
onStatusChanged?.call('状态: 已连接信令服务器,等待注册...');
};
_signaling.onMessage = _handleSignalMessage;
_signaling.onDisconnected = () {
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();
}
/// 确保 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;
if (deviceId == null) {
onStatusChanged?.call('状态: 未获取到本机设备ID连接中止');
return;
}
_webRtc = WebRtcController(
signaling: _signaling,
deviceId: deviceId,
targetDeviceId: targetDeviceId,
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。
_myDeviceId = message.fromDeviceId;
_initWebRtc();
// 拉取可连接的被控端绑定列表(仅已绑定设备)。
_loadBindings();
// 尝试用服务端 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;
}
/// 拉取本机可连接的被控端绑定列表(仅已绑定设备),供 UI 提示。
Future<void> _loadBindings() async {
try {
final data = await authRepository.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 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();
/// 断开连接并释放资源。
Future<void> disconnect() async {
_statsTimer?.cancel();
_statsTimer = null;
await _webRtc?.close();
_webRtc = null;
_signaling.disconnect();
}
}