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

flutter_webrtc二次呼叫报错:peerConnectionSetRemoteDescription状态异常

问题原因与解决方案

核心问题分析

第二次呼叫报错的根源是首次呼叫后的资源未彻底清理,导致旧的PeerConnection实例、Socket事件监听残留,第二次呼叫时操作了已处于closed/stable状态的旧连接。具体问题点:

  1. Socket事件监听未移除:每次创建控制器时绑定的IceCandidate、callAnswered等监听,退出时未销毁,第二次呼叫时会触发旧监听操作已关闭的PeerConnection。
  2. PeerConnection未置空:disposeConnection仅调用close()但未将_rtcPeerConnection设为null,第二次创建时可能复用旧实例引用。
  3. 媒体资源释放不彻底:forEach循环中使用await track.stop()无效(forEach不等待异步操作),导致轨道未完全停止就执行后续销毁逻辑。
  4. Ice候选列表未清空:第二次呼叫时rtcIceCandidates保留旧候选,可能引发错误。
  5. Renderer未彻底销毁:注释了_localRTCVideoRenderer.dispose()和_remoteRTCVideoRenderer.dispose(),导致资源泄漏。

具体修复代码

1. 完善资源销毁方法disposeConnection

Future disposeConnection() async {
  // 修复轨道停止的异步问题:用for循环替代forEach,确保每个track都完成stop
  if (_localStream != null) {
    for (var track in _localStream!.getVideoTracks()) {
      await track.stop();
    }
    for (var track in _localStream!.getAudioTracks()) {
      await track.stop();
    }
    await _localStream!.dispose();
    _localStream = null;
  }

  // 关闭并置空PeerConnection
  if (_rtcPeerConnection != null) {
    await _rtcPeerConnection!.close();
    _rtcPeerConnection = null;
  }

  // 销毁Renderer并置空引用
  await _localRTCVideoRenderer.dispose();
  await _remoteRTCVideoRenderer.dispose();

  // 清空Ice候选列表
  rtcIceCandidates.clear();

  // 移除所有Socket事件监听,避免残留
  socket!.off("IceCandidate");
  socket!.off("callAnswered");
  socket!.off("isExitCall");
  socket!.off("isCancelCall");
}

2. 修改setupPeerConnection中的Socket事件绑定逻辑

在绑定新的Socket监听前,先移除旧监听(避免重复绑定):

// for Incoming call
if (offer != null) {
  // 先移除旧监听
  socket!.off("IceCandidate");
  socket!.on("IceCandidate", (data) {
    String candidate = data["iceCandidate"]["candidate"];
    String sdpMid = data["iceCandidate"]["id"];
    int sdpMLineIndex = data["iceCandidate"]["label"];
    // 增加空判断,避免操作已销毁的PeerConnection
    if (_rtcPeerConnection != null) {
      _rtcPeerConnection!.addCandidate(RTCIceCandidate(
        candidate,
        sdpMid,
        sdpMLineIndex,
      ));
    }
  });

  // ... 其余代码不变
}
// for Outgoing call
else {
  // 先移除旧监听
  socket!.off("callAnswered");
  socket!.on("callAnswered", (data) {
    // 增加空判断
    if (_rtcPeerConnection != null) {
      _rtcPeerConnection!.setRemoteDescription(
        RTCSessionDescription(
          data["sdpAnswer"]["sdp"],
          data["sdpAnswer"]["type"],
        ),
      );
    }

    // ... 其余代码不变
  });
}

3. 完善onClose方法

确保销毁逻辑执行完成后再调用父类方法:

@override
void onClose() async {
  await disposeConnection();
  super.onClose();
}

4. 增加PeerConnection操作的空安全判断

在所有操作_rtcPeerConnection和_localStream的地方增加空判断,避免空指针或操作已销毁的实例:

toggleMic() {
  isAudioOn = !isAudioOn;
  if (_localStream != null) {
    _localStream!.getAudioTracks().forEach((track) {
      track.enabled = isAudioOn;
    });
  }
  update();
}

toggleCamera() {
  isVideoOn = !isVideoOn;
  if (_localStream != null) {
    _localStream!.getVideoTracks().forEach((track) {
      track.enabled = isVideoOn;
    });
  }
  update();
}

额外注意事项

  • 确保每次呼叫都创建全新的VideoCallController实例,不要复用旧控制器。
  • 检查Socket连接是否在每次呼叫后保持稳定,避免连接异常导致的事件监听混乱。

内容的提问来源于stack exchange,提问作者BodyKh

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 09:15:53