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

React-Native-WebRTC故障求助:接收Answer时App崩溃(FeathersJS后端)

WebRTC通话崩溃与连接故障排查指南

我仔细梳理了你的React Native代码,发现了几个核心问题,这些问题不仅会导致接收Answer时崩溃,还会阻碍WebRTC建立正常的peer连接。下面逐个分析并给出修复方案:

1. PeerConnection实例生命周期管理错误

你直接在函数组件顶层初始化localPC:

const localPC = new RTCPeerConnection(configuration);

函数组件每次渲染都会重新执行这段代码,导致创建全新的PeerConnection实例,之前的连接状态全部丢失。必须用useRef来持久化这个实例,确保组件渲染时不会重建:

import { useRef } from 'react';

// 替换原来的localPC初始化
const localPC = useRef<RTCPeerConnection | null>(null);
// 在需要的时候初始化(比如startCall或接收offer时)
const initPeerConnection = () => {
  if (!localPC.current) {
    localPC.current = new RTCPeerConnection(configuration);
  }
};

2. sendToPeer参数混乱导致数据错误

你的sendToPeer同时传递了sdp: payload和candidate: payload,这会让后端接收到的字段混淆。比如发送offer时,payload是SessionDescription,却被放到了candidate字段,接收answer时又错误地读取payload.candidate.sdp,这直接导致设置远程描述失败崩溃。

修复sendToPeer,根据消息类型传递正确的字段:

const sendToPeer = (messageType: any, payload: any) => {
  const data: any = {
    callType: messageType,
    caller: authState._id,
    answerer: userId,
  };
  // 根据消息类型设置对应字段
  if (messageType === 'offer' || messageType === 'answer') {
    data.sdp = payload;
  } else if (messageType === 'ice-candidate') {
    data.candidate = payload;
  }
  callService.create(data);
};

3. 接收Answer时的字段访问错误

原来的recieveAnswer错误地访问payload.candidate.sdp,实际上answer的SessionDescription在payload.sdp字段。同时必须用异步+错误捕获,避免崩溃:

const recieveAnswer = async (payload: any) => {
  try {
    console.log('recieved answer: ', payload.sdp)
    await localPC.current!.setRemoteDescription(new RTCSessionDescription(payload.sdp));
  } catch (err) {
    console.error('Failed to set remote answer:', err);
  }
};

4. ICE Candidate处理的多重错误

  • 绑定icecandidate事件重复监听:每次调用createIceCandidate都会重新绑定onicecandidate,导致重复发送ICE候选。应该只绑定一次,放在PeerConnection初始化后。
  • 添加ICE候选时格式错误:addIceCandidate需要传入RTCIceCandidate对象,不是原始的JSON数据。
  • 冗余的addStream调用:接收ICE时不需要重复添加本地流,应该在获取本地流后只添加一次。

修复ICE相关逻辑:

// 扩展initPeerConnection,添加ICE和流监听
const initPeerConnection = () => {
  if (!localPC.current) {
    localPC.current = new RTCPeerConnection(configuration);
    // 绑定ICE候选事件
    localPC.current.onicecandidate = e => {
      try {
        console.log('localPC icecandidate:', e.candidate);
        if (e.candidate) {
          sendToPeer('ice-candidate', e.candidate);
        }
      } catch (err) {
        console.error(`Error sending ICE candidate: ${err}`);
      }
    };
    // 用ontrack替代废弃的onaddstream
    localPC.current.ontrack = e => {
      if (e.streams[0] && remoteStream !== e.streams[0]) {
        console.log('Remote stream received', e.streams[0]);
        setRemoteStream(e.streams[0]);
      }
    };
  }
};

// 获取本地流后添加到PeerConnection
const startCall = async () => {
  console.log('start call function')
  initPeerConnection();
  const newStream = await mediaDevices.getUserMedia({
    audio: true,
    video: false,
  });
  setLocalStream(newStream);
  // 添加本地流到PeerConnection(用addTrack替代废弃的addStream)
  newStream.getTracks().forEach(track => {
    localPC.current!.addTrack(track, newStream);
  });
  await createOffer();
};

5. 流添加时机与API废弃问题

  • addStream和onaddstream是旧版API,已经被废弃,应该用addTrack和ontrack替代,确保兼容性。
  • 不能在recieveCall中依赖localStream状态(useState是异步的),应该在获取到本地流后立即添加到PeerConnection,避免流未就绪的问题。

6. 组件卸载时未清理监听事件

你的useEffect中注释掉了事件移除逻辑,这会导致组件卸载后仍然监听Feathers事件,引发内存泄漏甚至崩溃。必须恢复清理:

useEffect(() => {
  const handleIceCandidate = (incoming: any) => {
    try {
      if (incoming.data.candidate && localPC.current) {
        localPC.current.addIceCandidate(new RTCIceCandidate(incoming.data.candidate));
      }
    } catch (err) {
      console.error('Failed to add ICE candidate:', err);
    }
  };

  const handleOffer = async (payload: any) => {
    if (payload.data.answerer === authState._id) {
      setOfferData(payload.data);
      setCallAlert(true);
    }
  };

  const handleAnswer = async (payload: any) => {
    if (payload.data.answerer === authState._id) {
      await recieveAnswer(payload.data);
    }
  };

  callService.on('ice-candidate', handleIceCandidate);
  callService.on('offer', handleOffer);
  callService.on('answer', handleAnswer);

  return () => {
    callService.removeListener('ice-candidate', handleIceCandidate);
    callService.removeListener('offer', handleOffer);
    callService.removeListener('answer', handleAnswer);
    // 关闭PeerConnection和流
    if (localPC.current) {
      localPC.current.close();
      localPC.current = null;
    }
    localStream?.getTracks().forEach(track => track.stop());
  };
}, [callService, authState._id, userId]);

额外建议

  • 确保Feathers后端正确转发事件,建议添加callId标识区分不同通话,避免不同会话的消息混淆。
  • 增加更多错误捕获和日志输出,方便排查连接过程中的每一步问题。
  • 如果是内网环境测试,仅靠STUN服务器可能无法穿透NAT,需要配置TURN服务器。

内容的提问来源于stack exchange,提问作者Ping Kee Ng

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.08 14:57:33