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 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 _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 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 _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 _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 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(); }