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,277 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../../core/proto/control_message.pb.dart';
import '../../auth/data/auth_providers.dart';
import '../data/remote_controller.dart';
import '../data/self_codec_decoder.dart';
import '../data/video_recorder.dart';
import '../domain/connection_session_state.dart';
part 'connection_controller.g.dart';
/// 连接控制会话控制器:编排信令 + WebRTC管理控制端全部 UI 状态。
@Riverpod(keepAlive: true)
class ConnectionController extends _$ConnectionController {
/// 与 Windows 原生窗口通信的通道(用于按视频比例调整窗口高度)。
static const MethodChannel _windowChannel = MethodChannel('app/window');
RemoteController? _remote;
RTCVideoRenderer? _renderer;
final VideoRecorder _videoRecorder = VideoRecorder();
/// 记录上一次已应用的视频宽高比,避免重复调整窗口。
double _lastResizedAspect = 0;
@override
ConnectionSessionState build() {
ref.onDispose(() {
_videoRecorder.dispose();
_remote?.disconnect();
});
return const ConnectionSessionState(status: '状态: 已停止');
}
void _setStatus(String status) => state = state.copyWith(status: status);
void _alert(String message) => state = state.copyWith(alert: message);
void _clearAlert() => state = state.copyWith(alert: null);
/// 发起连接:先确保已登录,携带 Bearer 建立信令,成功后建立 WebRTC。
Future<void> connect({
required String serverUrl,
required String targetDeviceId,
required String authType,
required String authValue,
}) async {
if (serverUrl.isEmpty || targetDeviceId.isEmpty) {
_alert('请填写服务器地址和目标设备ID');
return;
}
final authRepository = ref.read(authRepositoryProvider);
state = state.copyWith(connecting: true, status: '状态: 正在连接信令服务器...');
_remote = RemoteController(
serverUrl: serverUrl,
targetDeviceId: targetDeviceId,
authRepository: authRepository,
);
_wireRemoteCallbacks();
await _remote!.connect(authType: authType, authValue: authValue);
}
void _wireRemoteCallbacks() {
final remote = _remote!;
remote.onStatusChanged = _setStatus;
remote.onConnectionEstablished = () {
state = state.copyWith(
connected: true,
connecting: false,
status: '状态: 已连接 - 远程控制中',
);
};
remote.onConnectionFailed = (error) {
state = state.copyWith(connecting: false);
_alert('连接失败:$error');
};
remote.onDisconnected = () {
state = state.copyWith(connected: false, connecting: false);
_setStatus('状态: 远端已断开');
};
remote.onIceDisconnected = (message) {
_alert(message);
disconnect();
};
remote.onTargetOffline = (message) {
_alert(message);
state = state.copyWith(connected: false, connecting: false);
};
remote.onConnectionRejected = (message) {
_alert(message);
state = state.copyWith(connected: false, connecting: false);
};
remote.onRemoteStream = (renderer) {
_renderer = renderer;
state = state.copyWith(renderer: renderer);
renderer.addListener(_onRendererUpdate);
if (state.pendingRecordStart) {
state = state.copyWith(pendingRecordStart: false);
_startRecording();
}
};
remote.onStats = (stats) => state = state.copyWith(stats: stats);
remote.onSelfCodecReady = (textureId) {
state = state.copyWith(
selfCodecTextureId: textureId,
selfCodecReady: true,
);
};
remote.onSelfCodecLost = () {
state = state.copyWith(selfCodecReady: false, selfCodecTextureId: null);
};
remote.onStreamModeReport = (mode) {
state = state.copyWith(streamMode: mode);
if (mode == SelfCodecDecoder.streamModeWebRtc && state.pendingRecordStart) {
state = state.copyWith(pendingRecordStart: false);
_startRecording();
}
};
remote.onResolutionReported = (w, h) {
if (w > 0 && h > 0) {
final aspect = w / h;
state = state.copyWith(videoAspect: aspect);
_resizeWindowToAspect(aspect);
}
};
remote.onFpsReport = (w, h, fps, supportedFps) {
state = state.copyWith(
lastReportedWidth: w > 0 ? w : state.lastReportedWidth,
lastReportedHeight: h > 0 ? h : state.lastReportedHeight,
currentFps: fps > 0 ? fps : state.currentFps,
fpsOptions: supportedFps.isNotEmpty ? supportedFps : state.fpsOptions,
);
};
remote.onSelfCodecNotSupported = () {
state = state.copyWith(selfCodecSupported: false);
_alert('当前平台不支持自编码硬解,已回退到 WebRTC 媒体流。');
};
remote.onTokenExpired = () {
_alert('登录已失效,请重新登录后再连接。');
_resetLogin();
};
remote.onForceLogout = () {
_alert('账号已在其他位置登录,已强制下线。');
_resetLogin();
};
}
Future<void> _resetLogin() async {
await ref.read(authRepositoryProvider).clear();
await disconnect();
state = state.copyWith(connected: false, connecting: false);
}
void _onRendererUpdate() {
final w = _renderer?.value.width ?? 0;
final h = _renderer?.value.height ?? 0;
if (w > 0 && h > 0) {
final aspect = w / h;
state = state.copyWith(videoAspect: aspect);
_resizeWindowToAspect(aspect);
}
}
/// Windows 端:保持窗口宽度不变,按视频宽高比调整窗口高度。
void _resizeWindowToAspect(double aspect) {
if (kIsWeb || defaultTargetPlatform != TargetPlatform.windows) return;
if (aspect <= 0) return;
if ((aspect - _lastResizedAspect).abs() < 0.001) return;
_lastResizedAspect = aspect;
_windowChannel.invokeMethod('resizeToVideoAspect', aspect);
}
/// 切换分辨率:写入状态并下发指令。
void selectResolution(int index) {
state = state.copyWith(selectedResolution: index);
final o = state.resolutionOptions[index];
_remote?.sendResolutionChange(
o['width'] as int,
o['height'] as int,
o['fps'] as int,
);
}
/// 切换帧率:仅切帧率,分辨率保持不变。
void selectFps(int fps) {
if (fps <= 0 || fps == state.currentFps) return;
_remote?.sendResolutionChange(
state.lastReportedWidth,
state.lastReportedHeight,
fps,
);
}
/// 切换串流模式WebRTC 全托管 <-> 自编码。
void toggleStreamMode() {
if (!state.selfCodecSupported) {
_alert('当前平台不支持自编码硬解。');
return;
}
final next = state.streamMode == SelfCodecDecoder.streamModeSelfCodec
? SelfCodecDecoder.streamModeWebRtc
: SelfCodecDecoder.streamModeSelfCodec;
state = state.copyWith(streamMode: next);
_remote?.sendStreamMode(next);
}
/// 切换远程视频录制:开始 / 停止。
Future<void> toggleRecord() async {
if (state.recording) {
await _stopRecording();
return;
}
final renderer = _renderer;
if (renderer == null) {
_alert('尚未连接或没有视频画面,无法录制');
return;
}
if (state.streamMode == SelfCodecDecoder.streamModeSelfCodec) {
// 自编码模式下没有 WebRTC 视频轨道,先切回标准模式再开始录制。
state = state.copyWith(
pendingRecordStart: true,
recordStatus: '正在切回标准模式以开始录制...',
);
_remote?.sendStreamMode(SelfCodecDecoder.streamModeWebRtc);
return;
}
await _startRecording();
}
Future<void> _startRecording() async {
if (state.recording) return;
final stream = _renderer?.srcObject;
if (stream == null) {
_alert('尚未接收到视频画面,无法录制');
return;
}
try {
final ok = await _videoRecorder.start(stream);
if (ok) {
state = state.copyWith(recording: true, recordStatus: '录制中...');
}
} catch (e) {
state = state.copyWith(recordStatus: '');
_alert('开始录制失败:$e');
}
}
Future<void> _stopRecording() async {
final path = await _videoRecorder.stop();
state = state.copyWith(
recording: false,
recordStatus: path != null ? '已保存:$path' : '录制已停止',
);
}
/// 发送控制指令protobuf 二进制)。
void sendControlCommand(ControlMessage command) {
_remote?.sendControlCommand(command);
}
/// 断开连接并复位全部 UI 状态。
Future<void> disconnect() async {
await _videoRecorder.dispose();
await _remote?.disconnect();
_remote = null;
_renderer?.removeListener(_onRendererUpdate);
_renderer = null;
_lastResizedAspect = 0;
state = const ConnectionSessionState(status: '状态: 已停止');
}
/// 消费一次性提示消息UI 展示后调用)。
void consumeAlert() => _clearAlert();
}