Flutter WebRTC应答SDP阶段onIceCandidate函数未触发问题排查
我帮你梳理下代码里导致应答端onIceCandidate完全不触发的几个关键问题,都是WebRTC开发里的常见坑:
1. 核心问题:应答端未设置本地SDP
WebRTC的ICE候选收集流程,必须在本地SDP被设置后才会启动——不管是发起端发offer,还是应答端发answer,都得把自己生成的SDP设置到PeerConnection里,ICE框架才会开始收集网络候选地址。
看你的_createAnswerSdp函数:你生成了应答SDP(description2),也把它上传到Firebase了,但偏偏漏掉了最关键的一步:await _peerConnection.setLocalDescription(description2);。没有这一步,ICE根本不会启动,自然不会触发onIceCandidate回调。
赶紧补上这行代码,位置就在你创建完description2之后:
RTCSessionDescription description2 = await _peerConnection.createAnswer({'offerToReceiveAudio': 1}); // 必须加这行!设置本地应答SDP,触发ICE收集 await _peerConnection.setLocalDescription(description2); Map<String, dynamic> session = {"sdp": description2.sdp}; firebaseInstance .doc(docId.text) .collection("sdp") .doc("answersdp") .set(session);
2. 回调覆盖的时机问题
你在_createPeerConnection里已经给PeerConnection绑定了一个onIceCandidate回调,之后在_createAnswerSdp里又重新赋值了这个回调。虽然这个操作本身没问题,但要注意时机:如果ICE收集在你重新赋值回调之前就已经开始,那早期的候选会被原来的回调处理,你在_createAnswerSdp里写的打印逻辑就接收不到这些候选。
建议要么统一在_createPeerConnection里处理所有ICE候选的上传逻辑,要么确保在setRemoteDescription之前就把应答端的回调设置好(不过先解决第一个问题,这个问题自然会缓解)。
3. 其他潜在的小问题
- 权限检查:Android端要确保已经申请了录音权限,没有音频权限的话,MediaStream创建失败,ICE流程也会受影响。在
AndroidManifest.xml里加上:
<uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.INTERNET" />
- 异步流程优化:你原来的代码用了
.then()嵌套,改成await写法会更清晰,也不容易出错(比如原来的代码里处理对方ICE候选的逻辑放在setRemoteDescription的then里,但其实应该等本地SDP设置完再处理)。
修正后的完整_createAnswerSdp示例
_createAnswerSdp() async { // 提前设置ICE回调,确保在ICE启动前生效 _peerConnection.onIceCandidate = (event) { print("Candidate detected: ${event.candidate}"); if (event.candidate != null) { Map<String, dynamic> icecandidate = { "candidate": event.candidate, "sdpMid": event.sdpMid, "sdpMlineIndex": event.sdpMlineIndex }; // 上传本地候选到Firebase await firebaseInstance .doc(docId.text) .collection("candidate") .doc() .set(icecandidate); print("Candidate uploaded: ${event.candidate}"); } }; // 获取远端offer SDP var offerDoc = await firebaseInstance .doc(docId.text) .collection("sdp") .doc("offersdp") .get(); var remoteSession = offerDoc.data()["sdp"]; RTCSessionDescription offerSdp = RTCSessionDescription(remoteSession, "offer"); // 设置远端offer await _peerConnection.setRemoteDescription(offerSdp); // 创建并设置本地应答SDP(核心步骤) RTCSessionDescription answerSdp = await _peerConnection.createAnswer({'offerToReceiveAudio': 1}); await _peerConnection.setLocalDescription(answerSdp); // 上传应答SDP到Firebase await firebaseInstance .doc(docId.text) .collection("sdp") .doc("answersdp") .set({"sdp": answerSdp.sdp}); // 添加对方已发送的ICE候选 var candidateDocs = await firebaseInstance .doc(docId.text) .collection("candidate") .get(); for (var doc in candidateDocs.docs) { var data = doc.data(); RTCIceCandidate candidate = RTCIceCandidate( data['candidate'], data['sdpMid'], data['sdpMlineIndex'] ); await _peerConnection.addCandidate(candidate); print("Added remote candidate: ${data['candidate']}"); } }
按照上面的修改,应答端的ICE流程应该就能正常启动,onIceCandidate回调也会被触发了。
内容的提问来源于stack exchange,提问作者hamere

