import 'package:flutter/foundation.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; import '../config/ice_servers.dart'; import '../models/signal_message.dart'; import '../signaling/signaling_client.dart'; /// 封装 WebRTC 连接逻辑(对应 Android 端 WebRtcClient)。 /// /// 职责: /// - 创建 PeerConnection(recvonly 视频 + 控制用 DataChannel) /// - 创建 Offer 并通过信令发送 /// - 处理 Answer / ICE 候选 /// - 通过 DataChannel 发送控制指令(触摸/滑动/按键) /// - 采集并解析连接统计信息 /// /// 本类同时兼容 Android 与 iOS(基于 flutter_webrtc)。 class WebRtcController { final SignalingClient signaling; final String deviceId; final String targetDeviceId; RTCPeerConnection? _pc; RTCDataChannel? _dataChannel; /// 远端视频渲染器,由调用方持有并显示。 final RTCVideoRenderer renderer = RTCVideoRenderer(); void Function()? onConnectionEstablished; void Function(String error)? onConnectionFailed; void Function()? onDisconnected; /// ICE 连接断开(如网络中断、被控端退出),用于提示用户并返回连接设置。 void Function(String message)? onIceDisconnected; /// 远端视频流就绪(renderer 已绑定视频轨道)。 void Function(RTCVideoRenderer renderer)? onRemoteStream; WebRtcController({ required this.signaling, required this.deviceId, required this.targetDeviceId, }); /// 初始化渲染器、PeerConnection,并创建 Offer。 Future initialize() async { await renderer.initialize(); final configuration = { 'iceServers': kIceServers, 'sdpSemantics': 'unified-plan', 'iceCandidatePoolSize': 10, // 增强复杂网络下的稳定性,参考 Android 端配置。 'continualGatheringPolicy': 'gatherContinually', 'iceTransportsType': 'all', 'tcpCandidatePolicy': 'enabled', }; _pc = await createPeerConnection(configuration); _pc!.onIceCandidate = _onIceCandidate; _pc!.onIceConnectionState = _onIceConnectionState; _pc!.onTrack = _onTrack; _pc!.onDataChannel = _onDataChannel; // 仅接收远端视频(recvonly)。 await _pc!.addTransceiver( kind: RTCRtpMediaType.RTCRtpMediaTypeVideo, init: RTCRtpTransceiverInit(direction: TransceiverDirection.RecvOnly), ); // 创建控制用 DataChannel。 // 使用非可靠、无序模式以降低延迟,解决“不跟手”问题。 final dcInit = RTCDataChannelInit() ..ordered = false ..maxRetransmits = 0; _dataChannel = await _pc!.createDataChannel(kDataChannelLabel, dcInit); _setupDataChannel(_dataChannel!); // 创建并发送 Offer。 final constraints = { 'mandatory': { 'OfferToReceiveVideo': true, 'OfferToReceiveAudio': false, }, 'optional': [], }; final offer = await _pc!.createOffer(constraints); await _pc!.setLocalDescription(offer); _sendOffer(offer.sdp!); } void _onTrack(RTCTrackEvent event) { if (event.track.kind == 'video' && event.streams.isNotEmpty) { renderer.srcObject = event.streams[0]; onRemoteStream?.call(renderer); } } void _onDataChannel(RTCDataChannel channel) { // 被控端也可能主动创建 DataChannel,统一处理。 _setupDataChannel(channel); } void _setupDataChannel(RTCDataChannel channel) { channel.onMessage = (RTCDataChannelMessage message) { // 处理来自被控端的消息(本控制端主要发送,此处仅做日志记录)。 // ignore: avoid_print print('DataChannel message: ${message.text}'); }; channel.onDataChannelState = (RTCDataChannelState state) { // ignore: avoid_print print('DataChannel state: $state'); }; } void _onIceCandidate(RTCIceCandidate candidate) { final payload = { 'sdpMid': candidate.sdpMid, 'sdpMLineIndex': candidate.sdpMLineIndex, 'candidate': candidate.candidate, }; final msg = SignalMessage.withPayload( type: 'ICE_CANDIDATE', fromDeviceId: deviceId, toDeviceId: targetDeviceId, deviceType: 'CONTROLLER', payload: payload, ); signaling.send(msg); } void _onIceConnectionState(RTCIceConnectionState state) { // ignore: avoid_print print('ICE connection state: $state'); if (state == RTCIceConnectionState.RTCIceConnectionStateConnected) { onConnectionEstablished?.call(); } else if (state == RTCIceConnectionState.RTCIceConnectionStateDisconnected) { onIceDisconnected?.call('ICE 连接已断开'); } else if (state == RTCIceConnectionState.RTCIceConnectionStateFailed) { onConnectionFailed?.call('ICE 连接失败'); onDisconnected?.call(); } } void _sendOffer(String sdp) { final payload = {'sdp': sdp}; final msg = SignalMessage.withPayload( type: 'OFFER', fromDeviceId: deviceId, toDeviceId: targetDeviceId, deviceType: 'CONTROLLER', payload: payload, ); signaling.send(msg); } /// 处理来自信令服务器的 Answer。 Future handleAnswer(String sdp) async { final answer = RTCSessionDescription(sdp, 'answer'); await _pc?.setRemoteDescription(answer); } /// 处理来自信令服务器的 ICE 候选。 Future handleIceCandidate(Map payload) async { final candidate = RTCIceCandidate( payload['candidate'] as String, payload['sdpMid'] as String?, payload['sdpMLineIndex'] as int?, ); await _pc?.addCandidate(candidate); } /// 通过 DataChannel 发送控制指令(JSON 字符串)。 void sendControlCommand(String commandJson) { if (_dataChannel?.state == RTCDataChannelState.RTCDataChannelOpen) { debugPrint('WebRtcController: Sending command -> $commandJson'); _dataChannel!.send(RTCDataChannelMessage(commandJson)); } } /// 采集连接统计信息,回调格式化后的文本(对应 Android 端 updateStats)。 Future getStatsText() async { if (_pc == null) return ''; final reports = await _pc!.getStats(); dynamic width = '-'; dynamic height = '-'; dynamic fps = '-'; dynamic delay = '-'; dynamic codec = '-'; for (final report in reports) { final values = report.values; if (report.type == 'inbound-rtp' && values['kind'] == 'video') { width = values['frameWidth'] ?? '-'; height = values['frameHeight'] ?? '-'; fps = values['framesPerSecond'] ?? '-'; final codecId = values['codecId']; if (codecId != null) { final codecReport = reports.where((r) => r.id == codecId).firstOrNull; final mime = codecReport?.values['mimeType']; if (mime is String && mime.startsWith('video/')) { codec = mime.substring(6); } } } else if (report.type == 'candidate-pair') { if (values['nominated'] == true) { final rtt = values['currentRoundTripTime']; if (rtt is num) { delay = (rtt * 1000).toStringAsFixed(0); } } } } return '分辨率: ${width}x$height 帧率: $fps 延迟: $delay ms\n' '解码格式: $codec'; } /// 释放所有资源。 Future close() async { try { await _dataChannel?.close(); } catch (_) {} try { await _pc?.close(); } catch (_) {} try { await renderer.dispose(); } catch (_) {} _dataChannel = null; _pc = null; } }