You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 18:53:10