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

React-Native-WebRTC iOS端黑屏及ICE候选、会话状态问题求助

React-Native-WebRTC三类异常问题排查与修复

问题描述

使用React-Native-WebRTC时遇到以下三类异常:

  • 所有通话的远程流均为黑屏,但音频和视频实际存在;
  • 第二次通话时出现addIceCandidate失败警告;
  • 第三次通话时提示因状态稳定无法添加Offer。

用户Hook实现代码:

import { useEffect, useState, useRef } from 'react';
import { RTCPeerConnection, RTCIceCandidate, RTCSessionDescription, mediaDevices, MediaStream } from 'react-native-webrtc';
import { useSharedConnection } from '../context/SignalingContext';
import { CertLocal, CertRequesNoEnct, EasyCall, decryptAESWithJson, encryptJsonWithAES } from './certRequest';

import RNCallKeep from 'react-native-callkeep';

import { useAuthContext } from '../context/authContext';
import { Soundi } from '../screens/videocalls/callingScreen';

const useWebRTCHook = () => {
  const [localStream, setLocalStream] = useState(null)
  const [remoteStream, setRemoteStream] = useState(null)
  const peerConnection = useRef(null);
  const target = useRef(null);
  const tyy = useRef(null);
  const UU = useRef(null);
  const { user, setIsInCall, setCallParams } = useAuthContext()
  const { connection } = useSharedConnection()
  const remoteCandidates = useRef([]);
  const [offerCreated, setOfferCreated] = useState(false);
  var Sound = new Soundi("cbs.mp3")


  const start = async (targetUserId, isVideo = "0", type = "calle",) => {
    tyy.current = type
    if (!offerCreated) {
      let peerConstraints = {
        iceServers: [
          {
            urls: "stun:XXXX",
          },
          {
            urls: "turn:XXXX",
            username: "cw1",
            credential: "XXXX"
          }

        ]

      };

      peerConnection.current = new RTCPeerConnection(peerConstraints);

      const mediaStream = await mediaDevices.getUserMedia({
        audio: true,
        video: true
      });
      mediaStream.getTracks().forEach(track => peerConnection.current.addTrack(track, mediaStream));
      setLocalStream(mediaStream);
      console.log('c-------------------->>>', targetUserId)
      let DataToSend = await encryptJsonWithAES({ Pid: "Patient/" + user.patid.toString(), Uuid: targetUserId }, user.s_aes, user.s_iv)
      let req = await CertLocal(DataToSend, user.token, "", "", "/communications/who-is-calling", user.certificate)
      let res = await decryptAESWithJson(req.replace("\"", ""), user.s_aes, user.s_iv)
      let i = JSON.parse(res)
      target.current = i.target
      UU.current = targetUserId


      if (type === "caller") {
        let offer = await peerConnection.current.createOffer()
        await peerConnection.current.setLocalDescription(offer)
        await connection.invoke('SendOffer', target.current, offer, peerConnection.current.localDescription, "video");
        setOfferCreated(true);
      }
    }

  };

  async function handleRemoteCandidate(iceCandidate) {
    iceCandidate = new RTCIceCandidate(iceCandidate);
    if (peerConnection.current.remoteDescription == null) return remoteCandidates.current.push(iceCandidate);
    return await peerConnection.current.addIceCandidate(iceCandidate);

  };

  async function processCandidates() {
    if (remoteCandidates.current.length < 1) { return; };
    remoteCandidates.current.map(candidate => handleRemoteCandidate(candidate));
    remoteCandidates.current = [];
  };



  useEffect(() => {
    if (connection) {
      connection.on('receiveoffer', async (userId, offer, description, type) => {
        if (peerConnection.current && target.current && peerConnection.current.remoteDescription === null && peerConnection.current.localDescription === null) {

          const offerDescription = new RTCSessionDescription(offer)
          await peerConnection.current.setRemoteDescription(offerDescription)

          const answerDescription = await peerConnection.current.createAnswer();
          await peerConnection.current.setLocalDescription(answerDescription);

          await connection.invoke('SendAnswer', target.current, answerDescription);

          // Process candidates after setting remote description
          await processCandidates();
        }
      })

      //connection
      connection.on("receiveicecandidate", async (userId, event) => {
        if (peerConnection.current) {
          console.log("receivedIce")
          if (!event.candidate || event.sdpMLineIndex === null || !event.sdpMid) return
          await handleRemoteCandidate(event);
        }
      })


      connection.on('ReceiveAnswer', async (userId, description) => {
        if (peerConnection.current && target.current) {
          const answerDescription = new RTCSessionDescription(description);
          await peerConnection.current.setRemoteDescription(answerDescription);

          await processCandidates()
        }
      })

      connection.on('uuidreject', (uuid) => hangleUp(uuid))
    }
    return () => {
      if (connection) {
        connection.off("'ReceiveAnswer")
        connection.off("'receiveoffer")
        connection.off("'SendAnswer")
        connection.off("'receiveicecandidate")
        connection.off("'uuidreject")
      }
    }
  }, [connection])



  useEffect(() => {

    if (peerConnection.current && connection) {


      peerConnection.current.addEventListener('icecandidate', event => SendICe(event));
      peerConnection.current.addEventListener('negotiationneeded', event => NegotiationNeeded(event));
      peerConnection.current.addEventListener('track', event => SetTrack(event));
    }

  }, [connection, peerConnection.current])


  async function SendICe(event) {
    if (!event.candidate) { return; };
    await connection.invoke('SendIceCandidate', target.current, event.candidate);
  }

  async function NegotiationNeeded(event) {
    console.log("negotiationneeded")
    if (tyy && tyy.current === "caller") {
      let offer = await peerConnection.current.createOffer()
      await peerConnection.current.setLocalDescription(offer)
      await connection.invoke('SendOffer', target.current, offer, peerConnection.current.localDescription, "video");
    }
  }
  async function SetTrack(event) {
    console.log("remoteTrack------------------", event.streams)
    setRemoteStream(event.streams[0])
  }


  const hangleUp = async (uuid) => {
    try {


      if (localStream) {
        localStream.getTracks().forEach(track => track.stop());
      }
      setLocalStream(null);
      setRemoteStream(null);
      setIsInCall(false)
      setCallParams(() => null)
      console.log("UU->>>>>>>>>>>>>>>>>>>>>", UU.current)
      RNCallKeep.endCall(UU.current);
      await EasyCall(UU.current + ".Pat" + user.patid, "/communications/reject-call")
      setOfferCreated(false)

      peerConnection.current.removeEventListener('icecandidate', event => SendICe(event));
      peerConnection.current.removeEventListener('negotiationneeded', event => NegotiationNeeded(event));
      peerConnection.current.removeEventListener('track', event => SetTrack(event));
      peerConnection.current.close();
      peerConnection.current = null; // Set peerConnection.current to null

    } catch (err) {
      console.log(err)
    }

  }
  const endCalli = async (id, uuid) => {
    try {

      if (localStream) {
        localStream.getTracks().forEach(track => track.stop());

      }
      setLocalStream(null);
      setRemoteStream(null);
      
      setOfferCreated(false)
      if(peerConnection.current){
        peerConnection.current.removeEventListener('icecandidate', event => SendICe(event));
        peerConnection.current.removeEventListener('negotiationneeded', event => NegotiationNeeded(event));
        peerConnection.current.removeEventListener('track', event => SetTrack(event));
        peerConnection.current.close();
        peerConnection.current = null; 
      }

    } catch (err) {
      console.log(err)
    }

  }

  const initiateCall = (targetUserId, userid, isVideoCall, uuid) => {
    connection.invoke('RequestVideoCall', targetUserId, userid.toString(), isVideoCall, uuid, "medux");
  };



  return { initiateCall, start, remoteStream, localStream, endCalli, hangleUp, setLocalStream, setRemoteStream };
};

export default useWebRTCHook;

更新:已修复ICE候选未处理问题,但出现单设备正常、另一设备异常的情况,两端Hook代码完全一致。


问题排查与修复

1. 远程流黑屏问题

原因:Track事件处理不完整,远程流的视频轨道未正确挂载,或<RTCView>绑定异常。
修复:

  • 优化SetTrack函数,确保轨道被正确添加到流中:
const remoteStreamRef = useRef(null);
async function SetTrack(event) {
  console.log("remoteTrack------------------", event.track);
  if (!remoteStreamRef.current) {
    const stream = new MediaStream();
    stream.addTrack(event.track);
    setRemoteStream(stream);
    remoteStreamRef.current = stream;
  } else {
    remoteStreamRef.current.addTrack(event.track);
    // 触发组件更新
    setRemoteStream(new MediaStream(remoteStreamRef.current.getTracks()));
  }
}
  • 确保<RTCView>正确绑定流并设置尺寸:
{remoteStream && <RTCView streamURL={remoteStream.toURL()} style={{ flex: 1 }} />}
  • 检查视频轨道状态,若被静音则手动启用:
if (event.track.kind === 'video' && !event.track.enabled) {
  event.track.enabled = true;
}

2. 第二次通话addIceCandidate失败

原因:通话结束后资源清理不彻底,残留旧的PeerConnection引用或ICE候选队列。
修复:

  • 在通话结束函数中清空ICE候选队列:
const hangleUp = async (uuid) => {
  try {
    // ... 现有清理代码
    remoteCandidates.current = []; // 清空候选队列
    setOfferCreated(false);
    // ... 其他操作
  } catch (err) {
    console.log(err)
  }
}
  • 增强handleRemoteCandidate的状态判断:
async function handleRemoteCandidate(iceCandidate) {
  if (!peerConnection.current || peerConnection.current.signalingState === "closed") return;
  iceCandidate = new RTCIceCandidate(iceCandidate);
  if (peerConnection.current.remoteDescription == null) {
    remoteCandidates.current.push(iceCandidate);
    return;
  }
  try {
    await peerConnection.current.addIceCandidate(iceCandidate);
  } catch (e) {
    console.warn("Failed to add ICE candidate:", e);
  }
};

3. 第三次通话提示状态稳定无法添加Offer

原因:offerCreated状态未正确重置,或negotiationneeded事件触发时PeerConnection已处于稳定状态。
修复:

  • 通话结束时重置所有状态变量:
const hangleUp = async (uuid) => {
  try {
    // ... 现有清理代码
    setOfferCreated(false);
    tyy.current = null; // 重置通话类型
    target.current = null;
    UU.current = null;
    // ... 关闭PeerConnection
  } catch (err) {
    console.log(err)
  }
}
  • 修改start函数的启动判断,避免复用旧连接:
const start = async (targetUserId, isVideo = "0", type = "calle") => {
  tyy.current = type;
  // 确保没有活跃连接且未创建过offer
  if (!offerCreated && !peerConnection.current) {
    // ... 创建PeerConnection等代码
  }
};
  • 优化NegotiationNeeded函数的状态判断:
async function NegotiationNeeded(event) {
  console.log("negotiationneeded");
  if (!tyy.current || tyy.current !== "caller") return;
  if (!peerConnection.current || peerConnection.current.signalingState !== "stable") return;
  try {
    let offer = await peerConnection.current.createOffer();
    await peerConnection.current.setLocalDescription(offer);
    await connection.invoke('SendOffer', target.current, offer, peerConnection.current.localDescription, "video");
  } catch (e) {
    console.error("Negotiation failed:", e);
  }
}

4. 单设备正常、另一设备异常问题

原因:信令事件监听移除错误,或ICE服务器配置不可用,信令消息时序异常。
修复:

  • 修正事件监听移除的引号错误:
return () => {
  if (connection) {
    connection.off("ReceiveAnswer")
    connection.off("receiveoffer")
    connection.off("SendAnswer")
    connection.off("receiveicecandidate")
    connection.off("uuidreject")
  }
}
  • 验证STUN/TURN服务器可用性,确保所有设备能正常访问;
  • 调整receiveoffer事件的处理逻辑,直接使用消息中的用户ID作为目标:
connection.on('receiveoffer', async (userId, offer, description, type) => {
  if (peerConnection.current && peerConnection.current.remoteDescription === null && peerConnection.current.localDescription === null) {
    target.current = userId; // 直接设置目标为发送方ID
    const offerDescription = new RTCSessionDescription(offer)
    await peerConnection.current.setRemoteDescription(offerDescription)
    const answerDescription = await peerConnection.current.createAnswer();
    await peerConnection.current.setLocalDescription(answerDescription);
    await connection.invoke('SendAnswer', target.current, answerDescription);
    await processCandidates();
  }
})

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 08:37:33