docs(webrtc_controller_flutter): 更新项目文档以反映重构后的架构
AGENTS.md 与 README.md 同步更新:根据实际代码结构重写目录树、技术栈、架构分层及编码规范,移除旧版内联示例并补充新的开发约定与代码生成命令。
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
import '../domain/signal_message.dart';
|
||||
|
||||
/// 信令客户端,封装 WebSocket 连接与消息收发。
|
||||
///
|
||||
/// 鉴权方式:浏览器/移动端原生 WebSocket 均可在握手阶段通过子协议
|
||||
/// (Sec-WebSocket-Protocol)传递 Bearer token:
|
||||
/// WebSocketChannel.connect(uri, protocols: ['signal.v1', 'auth.`<token>`'])
|
||||
/// 服务端从首子协议取出 auth.`<token>` 进行校验。
|
||||
///
|
||||
/// 不再发送 REGISTER —— 连接由服务端根据令牌身份自动完成,并下发 REGISTER_SUCCESS。
|
||||
class SignalingClient {
|
||||
final String serverUrl;
|
||||
final String? token;
|
||||
|
||||
WebSocketChannel? _channel;
|
||||
Timer? _heartbeatTimer;
|
||||
|
||||
/// 收到信令消息回调(已解析为 SignalMessage)。
|
||||
void Function(SignalMessage message)? onMessage;
|
||||
|
||||
/// 连接成功回调。
|
||||
void Function()? onConnected;
|
||||
|
||||
/// 连接断开回调。
|
||||
void Function()? onDisconnected;
|
||||
|
||||
/// 连接错误回调。
|
||||
void Function(String error)? onError;
|
||||
|
||||
/// 令牌失效(关闭码 4001):调用方应刷新令牌后重连。
|
||||
void Function()? onTokenExpired;
|
||||
|
||||
/// 强制下线(关闭码 4003):调用方应停止重连并跳登录。
|
||||
void Function()? onForceLogout;
|
||||
|
||||
SignalingClient({required this.serverUrl, this.token});
|
||||
|
||||
/// 建立 WebSocket 连接(携带 Bearer 子协议,不再发送 REGISTER)。
|
||||
void connect() {
|
||||
try {
|
||||
final uri = Uri.parse(serverUrl);
|
||||
final protocols = ['signal.v1'];
|
||||
if (token != null && token!.isNotEmpty) {
|
||||
protocols.add('auth.$token');
|
||||
}
|
||||
_channel = WebSocketChannel.connect(uri, protocols: protocols);
|
||||
|
||||
_channel!.stream.listen(
|
||||
_onData,
|
||||
onDone: _onDone,
|
||||
onError: (Object e) => onError?.call(e.toString()),
|
||||
cancelOnError: false,
|
||||
);
|
||||
|
||||
_startHeartbeat();
|
||||
onConnected?.call();
|
||||
} catch (e) {
|
||||
onError?.call(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
void _onData(dynamic data) {
|
||||
if (data is! String) return;
|
||||
try {
|
||||
final map = jsonDecode(data) as Map<String, dynamic>;
|
||||
final message = SignalMessage.fromJson(map);
|
||||
onMessage?.call(message);
|
||||
} catch (_) {
|
||||
// 忽略无法解析的消息。
|
||||
}
|
||||
}
|
||||
|
||||
void _onDone() {
|
||||
_stopHeartbeat();
|
||||
final code = _channel?.closeCode;
|
||||
if (code == 4001) {
|
||||
onTokenExpired?.call();
|
||||
return;
|
||||
}
|
||||
if (code == 4003) {
|
||||
onForceLogout?.call();
|
||||
return;
|
||||
}
|
||||
onDisconnected?.call();
|
||||
}
|
||||
|
||||
void _startHeartbeat() {
|
||||
_stopHeartbeat();
|
||||
_heartbeatTimer = Timer.periodic(const Duration(seconds: 25), (_) {
|
||||
if (_channel != null) {
|
||||
try {
|
||||
_channel!.sink.add(jsonEncode({'type': 'PING'}));
|
||||
} catch (_) {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _stopHeartbeat() {
|
||||
_heartbeatTimer?.cancel();
|
||||
_heartbeatTimer = null;
|
||||
}
|
||||
|
||||
/// 发送信令消息。
|
||||
void send(SignalMessage message) => _send(message);
|
||||
|
||||
void _send(SignalMessage message) {
|
||||
_channel?.sink.add(jsonEncode(message.toJson()));
|
||||
}
|
||||
|
||||
/// 关闭连接。
|
||||
void disconnect() {
|
||||
_stopHeartbeat();
|
||||
_channel?.sink.close();
|
||||
_channel = null;
|
||||
}
|
||||
|
||||
bool get isConnected => _channel != null;
|
||||
}
|
||||
Reference in New Issue
Block a user