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
相关产品推荐
相关产品推荐

