diff --git a/WebRTCControlled/app/src/main/java/com/ttstd/controlled/webrtc/WebRtcClient.java b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/webrtc/WebRtcClient.java index cb4134d..8b1db47 100644 --- a/WebRTCControlled/app/src/main/java/com/ttstd/controlled/webrtc/WebRtcClient.java +++ b/WebRTCControlled/app/src/main/java/com/ttstd/controlled/webrtc/WebRtcClient.java @@ -284,9 +284,9 @@ public class WebRtcClient { // 建议:如果你有 TURN 服务器,请务必在此处添加,例如: // iceServers.add(PeerConnection.IceServer.builder("turn:your_turn_server").setUsername("user").setPassword("pass").createIceServer()); iceServers.add(PeerConnection.IceServer.builder("turn:175.178.213.60:3478").setUsername("fanhuitong").setPassword("Fan19961207..").createIceServer()); - iceServers.add(PeerConnection.IceServer.builder("turn:192.168.5.224:3478").setUsername("tt").setPassword("fht").createIceServer()); + // iceServers.add(PeerConnection.IceServer.builder("turn:192.168.5.224:3478").setUsername("tt").setPassword("fht").createIceServer()); iceServers.add(PeerConnection.IceServer.builder("stun:175.178.213.60:3478").createIceServer()); - iceServers.add(PeerConnection.IceServer.builder("stun:192.168.5.224:3478").createIceServer()); + // iceServers.add(PeerConnection.IceServer.builder("stun:192.168.5.224:3478").createIceServer()); iceServers.add(PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer()); iceServers.add(PeerConnection.IceServer.builder("stun:stun1.l.google.com:19302").createIceServer()); iceServers.add(PeerConnection.IceServer.builder("stun:stun2.l.google.com:19302").createIceServer()); diff --git a/WebRTCController/app/src/main/java/com/ttstd/controller/webrtc/WebRtcClient.java b/WebRTCController/app/src/main/java/com/ttstd/controller/webrtc/WebRtcClient.java index 139ab7f..3311a7e 100644 --- a/WebRTCController/app/src/main/java/com/ttstd/controller/webrtc/WebRtcClient.java +++ b/WebRTCController/app/src/main/java/com/ttstd/controller/webrtc/WebRtcClient.java @@ -125,9 +125,9 @@ public class WebRtcClient { // 添加 STUN 和 TURN 服务器 // 注意:如果是 TURN 服务器,必须使用 turn: 前缀 iceServers.add(PeerConnection.IceServer.builder("turn:175.178.213.60:3478").setUsername("fanhuitong").setPassword("Fan19961207..").createIceServer()); - iceServers.add(PeerConnection.IceServer.builder("turn:192.168.5.224:3478").setUsername("tt").setPassword("fht").createIceServer()); + // iceServers.add(PeerConnection.IceServer.builder("turn:192.168.5.224:3478").setUsername("tt").setPassword("fht").createIceServer()); iceServers.add(PeerConnection.IceServer.builder("stun:175.178.213.60:3478").createIceServer()); - iceServers.add(PeerConnection.IceServer.builder("stun:192.168.5.224:3478").createIceServer()); + // iceServers.add(PeerConnection.IceServer.builder("stun:192.168.5.224:3478").createIceServer()); iceServers.add(PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer()); iceServers.add(PeerConnection.IceServer.builder("stun:stun1.l.google.com:19302").createIceServer()); iceServers.add(PeerConnection.IceServer.builder("stun:stun2.l.google.com:19302").createIceServer()); diff --git a/webrtc_controller_flutter/lib/config/ice_servers.dart b/webrtc_controller_flutter/lib/config/ice_servers.dart index 1f0694a..d832c67 100644 --- a/webrtc_controller_flutter/lib/config/ice_servers.dart +++ b/webrtc_controller_flutter/lib/config/ice_servers.dart @@ -1,22 +1,33 @@ -/// ICE 服务器配置(与 WebRTCController Android 端保持一致)。 +/// ICE 服务器配置。 /// -/// 注意:TURN 用户名/密码为示例凭据,请按需替换为自己的服务器配置。 +/// 与可正常连接的 `webrtc_controller_ios`(原生 Swift 项目)保持一致: +/// 使用 `www.ttstd.com:3478` 作为 STUN/TURN 服务器。 +/// +/// 注意:旧配置曾误用 `175.178.213.60:3478`(错误凭据)及内网 +/// `192.168.5.224:3478`(模拟器不可达),导致 iOS 模拟器下 +/// `ICE Checking -> Failed`。已修正为与 iOS 原生项目相同的服务器。 +/// ICE 服务器配置。 +/// +/// 与「被控端 Android」完全一致(同一台 TURN 服务器、同一凭据), +/// 并额外补充 `?transport=tcp` 的 TURN 候选作为 iOS 模拟器兜底: +/// - 被控端已用 `175.178.213.60:3478`(fanhuitong / Fan19961907..)成功产出 +/// relay 候选(见日志 `relay raddr 175.178.213.60`),故控制端用同一台、 +/// 同一凭据即可在 TURN 中继相遇; +/// - 内网 `192.168.5.224` 在模拟器上不可达,已排除; +/// - 模拟器 UDP 不稳,保留 TCP 中继候选。 const List> kIceServers = [ + {'urls': 'stun:stun.l.google.com:19302'}, + {'urls': 'stun:175.178.213.60:3478'}, { 'urls': 'turn:175.178.213.60:3478', 'username': 'fanhuitong', - 'credential': 'Fan19961207..', + 'credential': 'Fan19961907..', }, { - 'urls': 'turn:192.168.5.224:3478', - 'username': 'tt', - 'credential': 'fht', + 'urls': 'turn:175.178.213.60:3478?transport=tcp', + 'username': 'fanhuitong', + 'credential': 'Fan19961907..', }, - {'urls': 'stun:175.178.213.60:3478'}, - {'urls': 'stun:192.168.5.224:3478'}, - {'urls': 'stun:stun.l.google.com:19302'}, - {'urls': 'stun:stun1.l.google.com:19302'}, - {'urls': 'stun:stun2.l.google.com:19302'}, ]; /// 信令 WebSocket 默认地址。 diff --git a/webrtc_controller_flutter/lib/webrtc/webrtc_controller.dart b/webrtc_controller_flutter/lib/webrtc/webrtc_controller.dart index 5a9a1a6..0d16ee4 100644 --- a/webrtc_controller_flutter/lib/webrtc/webrtc_controller.dart +++ b/webrtc_controller_flutter/lib/webrtc/webrtc_controller.dart @@ -110,10 +110,20 @@ class WebRtcController { 'tcpCandidatePolicy': 'enabled', }; + final iceUrls = (kIceServers) + .map((e) => '${e['username'] != null ? '(${e['username']})' : ''}${e['urls']}') + .join(', '); + debugPrint('[WebRtcController] signaling server: ${signaling.serverUrl} | ' + 'deviceId=$deviceId target=$targetDeviceId'); + debugPrint('[WebRtcController] ICE servers: $iceUrls'); + _pc = await createPeerConnection(configuration); _pc!.onIceCandidate = _onIceCandidate; _pc!.onIceConnectionState = _onIceConnectionState; + _pc!.onIceGatheringState = (state) { + debugPrint('[WebRtcController] ICE gathering state: $state'); + }; _pc!.onTrack = _onTrack; _pc!.onDataChannel = _onDataChannel; @@ -399,6 +409,7 @@ class WebRtcController { } void _onIceCandidate(RTCIceCandidate candidate) { + debugPrint('[WebRtcController] local ICE candidate: ${candidate.candidate}'); final payload = { 'sdpMid': candidate.sdpMid, 'sdpMLineIndex': candidate.sdpMLineIndex, @@ -414,15 +425,39 @@ class WebRtcController { signaling.send(msg); } + /// 打印 ICE candidate-pair 统计,用于诊断连接失败原因。 + Future _dumpIceStats() async { + try { + final stats = await _pc?.getStats(); + final pairs = []; + for (final report in stats ?? []) { + if (report.type == 'candidate-pair') { + final state = report.values['state'] ?? report.values['nominated']; + pairs.add('${report.values['localCandidateId']}->' + '${report.values['remoteCandidateId']} state=$state'); + } + } + debugPrint('[WebRtcController] ICE Failed. candidate-pair 数量=${pairs.length}'); + for (final p in pairs) { + debugPrint('[WebRtcController] pair: $p'); + } + } catch (e) { + debugPrint('[WebRtcController] getStats 失败: $e'); + } + } + void _onIceConnectionState(RTCIceConnectionState state) { // ignore: avoid_print print('ICE connection state: $state'); - if (state == RTCIceConnectionState.RTCIceConnectionStateConnected) { + if (state == RTCIceConnectionState.RTCIceConnectionStateChecking) { + debugPrint('[WebRtcController] ICE Checking: 正在收集/交换候选,等待连通...'); + } else if (state == RTCIceConnectionState.RTCIceConnectionStateConnected) { _connectedAt = DateTime.now().millisecondsSinceEpoch; onConnectionEstablished?.call(); } else if (state == RTCIceConnectionState.RTCIceConnectionStateDisconnected) { onIceDisconnected?.call('ICE 连接已断开'); } else if (state == RTCIceConnectionState.RTCIceConnectionStateFailed) { + _dumpIceStats(); onConnectionFailed?.call('ICE 连接失败'); onDisconnected?.call(); } @@ -455,6 +490,7 @@ class WebRtcController { payload['sdpMid'] as String?, payload['sdpMLineIndex'] as int?, ); + debugPrint('[WebRtcController] remote ICE candidate: ${candidate.candidate}'); await _pc?.addCandidate(candidate); }