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/binding_device.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); /// 仅建立信令(WebSocket)连接,**不**发起远程控制。 /// /// 进入设置页即可调用;重复调用会被忽略。 Future connectSignaling({required String serverUrl}) async { if (serverUrl.isEmpty) { _alert('信令服务器地址为空'); return; } if (_remote != null || state.signalingConnected) return; final authRepository = ref.read(authRepositoryProvider); state = state.copyWith(connecting: true, status: '状态: 正在连接信令服务器...'); _remote = RemoteController( serverUrl: serverUrl, authRepository: authRepository, ); _wireRemoteCallbacks(); await _remote!.connectSignaling(); } /// 发起远程控制:需信令已就绪,向目标设备创建 Offer。 Future startControl({ required String targetDeviceId, required String authType, required String authValue, }) async { if (targetDeviceId.isEmpty) { _alert('请填写目标设备ID'); return; } if (!state.signalingConnected || _remote == null) { _alert('信令未就绪,请稍后重试'); return; } state = state.copyWith(connecting: true, status: '状态: 正在发起连接...'); _remote!.startControl( target: targetDeviceId, authType: authType, authValue: authValue, ); } /// 拉取已绑定设备列表,并合并在线设备状态。 /// /// 参考 Android 控制端的 [MainViewModel.loadBindings] / [getOnlineDevices], /// 用在线列表标记绑定设备是否可立即连接。 Future loadDevices() async { final authRepository = ref.read(authRepositoryProvider); state = state.copyWith(deviceRefreshing: true, deviceError: null); try { final bindings = await authRepository.getBindings(); List online = const []; try { online = await authRepository.getOnlineDevices(); } on Object { // 在线列表接口失败时,仍展示全部绑定(仅不标记在线)。 } final onlineUids = {for (final d in online) d.deviceUid}; final merged = [ for (final b in bindings) b.copyWith(online: onlineUids.contains(b.deviceUid)), ]; final stillSelected = merged .where((b) => b.deviceUid == state.selectedDeviceUid) .isNotEmpty; state = state.copyWith( bindings: merged, onlineUids: onlineUids, deviceRefreshing: false, selectedDeviceUid: stillSelected ? state.selectedDeviceUid : null, ); } on Object catch (e) { debugPrint(e.toString()); state = state.copyWith( deviceRefreshing: false, deviceError: '加载绑定设备失败:$e', ); } } /// 在绑定设备列表中选择一个目标设备(仅记录选择,不下发连接)。 void selectDevice(String uid) { if (state.selectedDeviceUid == uid) return; state = state.copyWith(selectedDeviceUid: uid); } /// 当前选中的绑定设备(无则返回 null)。 BindingDevice? get selectedDevice { final uid = state.selectedDeviceUid; if (uid == null) return null; for (final b in state.bindings) { if (b.deviceUid == uid) return b; } return null; } /// 向选中的绑定设备发起远程控制。 /// /// 鉴权方式取自 [BindingDevice.authType],凭据由 UI 的鉴权对话框收集后传入。 Future connectSelectedDevice({ required String authType, required String authValue, }) async { final target = selectedDevice; if (target == null) { _alert('请先选择一个绑定的设备'); return; } if (!target.online) { _alert('设备「${target.name}」当前不在线,无法连接'); return; } await startControl( targetDeviceId: target.deviceUid, authType: authType, authValue: authValue, ); } /// 使用被控端展示的配对码建立绑定关系,并刷新绑定设备列表。 Future redeemPairingCode(String code) async { final trimmed = code.trim(); if (trimmed.isEmpty) { state = state.copyWith(redeemError: '请输入配对码'); return false; } final authRepository = ref.read(authRepositoryProvider); state = state.copyWith(redeeming: true, redeemError: null); try { await authRepository.redeemPairingCode(trimmed); state = state.copyWith(redeeming: false); await loadDevices(); return true; } on Object catch (e) { state = state.copyWith(redeeming: false, redeemError: '绑定失败:$e'); return false; } } /// 清除配对码兑换错误提示。 void clearRedeemError() => state.copyWith(redeemError: null); void _wireRemoteCallbacks() { final remote = _remote!; remote.onStatusChanged = _setStatus; remote.onRegistered = (myDeviceId) { state = state.copyWith( signalingConnected: true, connecting: false, myDeviceId: myDeviceId, ); }; 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, signalingConnected: false, myDeviceId: null, ); _setStatus('状态: 远端已断开'); }; // 以下三种情况仅结束远程控制,保留信令连接以便重新发起。 remote.onIceDisconnected = (message) { _alert(message); stopControl(); }; remote.onTargetOffline = (message) { _alert(message); stopControl(); }; remote.onConnectionRejected = (message) { _alert(message); stopControl(); }; 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); } /// 结束远程控制但保持信令连接,便于重新选择设备发起连接。 Future stopControl() async { await _videoRecorder.dispose(); await _remote?.stopControl(); _renderer?.removeListener(_onRendererUpdate); _renderer = null; _lastResizedAspect = 0; final signalingAlive = state.signalingConnected; state = ConnectionSessionState( status: signalingAlive ? '状态: 已连接信令服务器' : '状态: 已停止', signalingConnected: signalingAlive, myDeviceId: state.myDeviceId, alert: state.alert, ); } /// 断开全部连接(含信令)并复位 UI 状态。 Future disconnect() async { await _videoRecorder.dispose(); await _remote?.disconnect(); _remote = null; _renderer?.removeListener(_onRendererUpdate); _renderer = null; _lastResizedAspect = 0; state = ConnectionSessionState(status: '状态: 已停止', alert: state.alert); } /// 消费一次性提示消息(UI 展示后调用)。 void consumeAlert() => _clearAlert(); }