Files
VibeCoding/webrtc_controller_flutter/lib/features/connection/presentation/connection_controller.dart
tongtongstudio d5e66a1777 docs(webrtc_controller_flutter): 更新项目文档以反映重构后的架构
AGENTS.md 与 README.md 同步更新:根据实际代码结构重写目录树、技术栈、架构分层及编码规范,移除旧版内联示例并补充新的开发约定与代码生成命令。
2026-08-03 16:12:44 +08:00

278 lines
9.0 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
}