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

WebRTC PeerConnection含ICE候选仍连接失败问题排查

WebRTC P2P连接ICE失败问题排查(React + Python)

我正在尝试用运行Python脚本的摄像头设备和React应用建立P2P连接,实现摄像头视频流展示。双方交换的Offer和Answer都包含ICE候选,但ICE连接始终失败。React端收到Answer后,iceconnectionstate先变为checking,几秒后变成failed,错误提示是WebRTC: ICE failed, add a TURN server and see about:webrtc for more details。

之前用Socket.io做信令时能正常运行,现在换成Django Channels(WebSocket)就出问题,而且两端在同一局域网。另外一个奇怪的现象是:如果React端连续发两次Offer,连接能短暂建立几秒后断开。


React端RTCPeerConnection及Offer发送逻辑

const connection = new RTCPeerConnection({
      iceServers: [
        {
          urls: "stun:stun.l.google.com:19302",
        },
      ],
    });

const socket = new WebSocket(signal-url);

socket.onopen = () => {
      const constraints = {
        offerToReceiveAudio: true,
        offerToReceiveVideo: true,
      };

      connection.onicegatheringstatechange  = (event) => {
          if (connection.iceGatheringState === "complete") {
             const offer = {
              type: connection.localDescription.type,
              sdp: connection.localDescription.sdp,
            };
            socket.send(JSON.stringify(offer));
          }
      }

      connection.addEventListener(
        "track",
        (evt) => {
          ...
        },
        false
      )

      connection
        .createOffer(constraints)
        .then((offer) => {
          return connection.setLocalDescription(offer);
        })
        .catch((error) => {
          console.log(error);
        });

      socket.onmessage = (message:any) => {
          const msg_json = JSON.parse(message.data);
          if (msg_json.type === "answer") {
            connection.setRemoteDescription(msg_json);
            connection.addTransceiver("video");
          }
      }

}

Python摄像头端脚本逻辑

def on_message(ws, message):
        loop = asyncio.get_event_loop()
        loop.run_until_complete(self.on_message_async(message))

async def on_message_async(self, message):
        message_json = json.loads(message)
        if message_json['type'] == 'offer':
            connection = RTCPeerConnection(
                configuration=RTCConfiguration(
                    iceServers=[
                        RTCIceServer(
                            urls=['stun:stun.l.google.com:19302']
                        )
                    ]
                )
            )
            rtc_offer = RTCSessionDescription(sdp=message_json["sdp"], type=message_json['type'])
            await connection.setRemoteDescription(rtc_offer)

            try:
                senders = connection.getSenders()
                kinds = set()
                for sender in senders:
                    kinds.add(sender.kind)
                if 'video' in kinds:
                    connection.pc.addTrack(vid_track)

            except Exception as e:
                print(e) 

            answer = await connection.createAnswer()
            await connection.pc.setLocalDescription(answer)
            try:
                json_desc = {"type":connection.localDescription.type,        "sdp":connection.localDescription.sdp}
                desc = json.dumps(json_desc)
                ws.send(desc)
            except Exception as e:
                print(e)

ws = websocket.WebSocketApp(signal_server_url,
                                on_open=on_open,
                                on_message=on_message,
                                on_error=on_error,
                                on_close=on_close)

问题分析及修复方案

1. ICE候选交换时机错误

你现在是等iceGatheringState变为complete才发送Offer,这会错过早期可用的局域网主机候选,导致ICE连接超时。WebRTC的最佳实践是边收集ICE候选边交换,而不是等全部收集完成。

  • 修复:监听onicecandidate事件,每收到一个候选就通过WebSocket发送给对方;同时Offer不用等ICE收集完成,设置完LocalDescription就立即发送。

2. Python端添加Track逻辑完全错误

刚设置完RemoteDescription时,connection.getSenders()里不可能有video类型的sender,导致addTrack代码根本不会执行——Python端没发送视频流,ICE自然会因为没有媒体流而失败。

  • 修复:去掉多余的检查逻辑,创建PeerConnection后直接添加视频Track,再处理Offer和Answer。

3. React端添加Transceiver时机太晚

你在收到Answer后才调用addTransceiver("video"),但Offer里已经声明了要接收视频,这时候添加完全没必要,反而可能导致媒体协商不匹配。

  • 修复:在创建Offer之前就添加Transceiver,或者依赖Offer的constraints声明即可。

4. Python端PeerConnection属性访问错误

代码里出现connection.pc.addTrack和connection.pc.setLocalDescription,如果connection本身是RTCPeerConnection实例,.pc属于多余的属性访问,会抛出异常(被catch吃掉),导致Answer的LocalDescription没正确设置。

  • 修复:直接用connection.addTrack和await connection.setLocalDescription(answer)。

5. 缺少ICE候选的双向交换

当前代码只交换了Offer/Answer,没有处理对方发送的ICE候选,这是ICE连接成功的必要条件。

  • 修复:两端都要监听icecandidate事件发送候选,同时在收到对方的ICE候选消息时调用addIceCandidate。

修改后的代码示例

React端修改后代码

const connection = new RTCPeerConnection({
  iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
});

const socket = new WebSocket(signal-url);

socket.onopen = () => {
  // 提前添加接收视频/音频的Transceiver
  connection.addTransceiver("video", { direction: "recvonly" });
  connection.addTransceiver("audio", { direction: "recvonly" });

  // 实时发送ICE候选
  connection.onicecandidate = (event) => {
    if (event.candidate) {
      socket.send(JSON.stringify({
        type: "ice-candidate",
        candidate: event.candidate
      }));
    }
  };

  connection.addEventListener("track", (evt) => {
    if (evt.track.kind === "video") {
      // 把视频流绑定到页面的video元素
      document.getElementById("remote-video").srcObject = evt.streams[0];
    }
  });

  const constraints = {
    offerToReceiveAudio: true,
    offerToReceiveVideo: true,
  };

  // 设置完LocalDescription就发送Offer,不等ICE收集完成
  connection.createOffer(constraints)
    .then((offer) => connection.setLocalDescription(offer))
    .then(() => {
      socket.send(JSON.stringify({
        type: connection.localDescription.type,
        sdp: connection.localDescription.sdp
      }));
    })
    .catch((error) => console.log(error));

  socket.onmessage = (message) => {
    const msg_json = JSON.parse(message.data);
    if (msg_json.type === "answer") {
      connection.setRemoteDescription(msg_json);
    } else if (msg_json.type === "ice-candidate") {
      // 处理对方发送的ICE候选
      connection.addIceCandidate(new RTCIceCandidate(msg_json.candidate));
    }
  };
};

Python端修改后代码

def on_message(ws, message):
    loop = asyncio.get_event_loop()
    # 把ws传入,方便发送ICE候选
    loop.run_until_complete(self.on_message_async(ws, message))

async def on_message_async(self, ws, message):
    message_json = json.loads(message)
    if message_json['type'] == 'offer':
        connection = RTCPeerConnection(
            configuration=RTCConfiguration(
                iceServers=[RTCIceServer(urls=['stun:stun.l.google.com:19302'])]
            )
        )

        # 先添加视频Track,这是核心!
        try:
            connection.addTrack(self.vid_track)
        except Exception as e:
            print(f"添加视频Track失败: {e}")

        # 实时发送ICE候选给React端
        @connection.on("icecandidate")
        async def on_icecandidate(candidate):
            if candidate:
                ws.send(json.dumps({
                    "type": "ice-candidate",
                    "candidate": candidate
                }))

        # 设置RemoteDescription
        rtc_offer = RTCSessionDescription(sdp=message_json["sdp"], type=message_json['type'])
        await connection.setRemoteDescription(rtc_offer)

        # 创建并发送Answer
        answer = await connection.createAnswer()
        await connection.setLocalDescription(answer)  # 去掉多余的.pc
        try:
            json_desc = {
                "type": connection.localDescription.type,
                "sdp": connection.localDescription.sdp
            }
            ws.send(json.dumps(json_desc))
        except Exception as e:
            print(f"发送Answer失败: {e}")
    elif message_json['type'] == 'ice-candidate':
        # 处理React端发送的ICE候选
        await connection.addIceCandidate(RTCIceCandidate(message_json['candidate']))

ws = websocket.WebSocketApp(signal_server_url,
                            on_open=on_open,
                            on_message=on_message,
                            on_error=on_error,
                            on_close=on_close)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 11:42:34