React Native Expo中WebRTC视频通话连接正常但视频流异常求助
React Native Expo WebRTC视频流问题修复方案
环境信息
- 依赖版本:
@config-plugins/react-native-webrtc": "^7.0.0react-native-webrtc": "^111.0.3
- 测试环境:Android模拟器(API level 34,镜像:system-images/android-34/google_apis/x86_x64)
当前Peer连接逻辑正常,但视频流无法正常显示,以下是问题排查和修复方案:
核心问题分析
- 重复获取本地媒体流:
useEffect中的initLocalStream和createPeerConnection都调用了getUserMedia,导致重复请求媒体资源,引发流实例冲突。 - 缺少RTCView渲染组件:原代码未包含
RTCView的渲染逻辑,这是WebRTC视频流在React Native中显示的必要组件。 - 媒体权限配置缺失:Expo环境下需确保已正确配置相机和麦克风权限。
- 远程流处理不严谨:
ontrack事件中直接设置远程流,未考虑多轨道或流更新场景;setRemoteDescription未添加异步等待逻辑。
修正后的完整代码
import { mediaDevices, RTCPeerConnection, RTCView, RTCIceCandidate, RTCSessionDescription, MediaStream, } from 'react-native-webrtc'; import styles from './callPageStyle'; import { useEffect, useState, useRef } from "react"; import { useSelector, useDispatch } from 'react-redux'; import { View, Button } from 'react-native'; const CallPage = ({ route, navigation }) => { const userData = useSelector((state) => state.userData); const dispatch = useDispatch(); const { callerId, calleeId, calleeName, isVideo, isCreator } = route.params; const [localStream, setLocalStream] = useState(null); const [remoteStream, setRemoteStream] = useState(null); const [callStarted, setCallStarted] = useState(false); const peerConnectionRef = useRef(null); const remoteRTCMessage = useRef(null); useEffect(() => { const recipientID = isCreator ? calleeId : callerId; webSocketService.registerSendCallMessageCallback(recipientID, onNewCall); webSocketService.registerAnswerCallMessageCallback(recipientID, onCallAnswered); webSocketService.registerICECandidateMessageCallback(recipientID, onICEcandidate); // 仅初始化一次本地流,避免重复请求 const initLocalStream = async () => { try { const stream = await mediaDevices.getUserMedia({ audio: true, video: isVideo ? { width: 640, height: 480 } : false }); setLocalStream(stream); } catch (err) { console.error("获取本地媒体流失败:", err); } }; const delayProcessCall = setTimeout(() => { if (!isCreator) { startCall(); } else { initLocalStream(); } }, 2000); return () => { clearTimeout(delayProcessCall); // 清理媒体资源 if (localStream) { localStream.getTracks().forEach(track => track.stop()); } if (peerConnectionRef.current) { peerConnectionRef.current.close(); peerConnectionRef.current = null; } }; }, [isCreator, isVideo]); const createPeerConnection = async () => { if (peerConnectionRef.current) return; const configuration = { iceServers: [ { urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' }, ], }; const peerConnection = new RTCPeerConnection(configuration); // 添加已获取的本地流轨道,避免重复请求 if (localStream) { localStream.getTracks().forEach(track => peerConnection.addTrack(track, localStream)); } peerConnection.onicecandidate = (event) => { if (event.candidate) { webSocketService.sendICECandidateToPeerServer(userData.id, isCreator ? calleeId : callerId, { calleeId: isCreator ? calleeId : callerId, rtcMessage: { label: event.candidate.sdpMLineIndex, id: event.candidate.sdpMid, candidate: event.candidate.candidate, }, }); } else { console.log("ICE候选集发送完毕"); } }; peerConnection.ontrack = (event) => { // 确保远程流正确更新 if (!remoteStream || event.streams[0].id !== remoteStream.id) { setRemoteStream(event.streams[0]); } }; peerConnectionRef.current = peerConnection; }; const startCall = async () => { try { console.log("发起新呼叫"); await createPeerConnection(); const offer = await peerConnectionRef.current.createOffer(); await peerConnectionRef.current.setLocalDescription(offer); webSocketService.sendNewCallToPeerServer(userData.id, isCreator ? calleeId : callerId, { calleeId: isCreator ? calleeId : callerId, rtcMessage: offer, }); } catch (error) { console.error('创建Offer失败:', error); } }; const handleAnswer = async () => { try { const answer = await peerConnectionRef.current.createAnswer(); await peerConnectionRef.current.setLocalDescription(answer); webSocketService.sendAnswerCallToPeerServer(userData.id, isCreator ? calleeId : callerId, { calleeId: isCreator ? calleeId : callerId, rtcMessage: answer, }); setCallStarted(true); } catch (error) { console.error('创建Answer失败:', error); } }; const onNewCall = async (payload) => { console.log("收到新呼叫"); const bigData = JSON.parse(payload.body); const data = JSON.parse(bigData.data); remoteRTCMessage.current = data.rtcMessage; await createPeerConnection(); await peerConnectionRef.current.setRemoteDescription( new RTCSessionDescription(remoteRTCMessage.current) ); handleAnswer(); }; const onCallAnswered = async (payload) => { console.log("呼叫已接通"); const bigData = JSON.parse(payload.body); const data = JSON.parse(bigData.data); remoteRTCMessage.current = data.rtcMessage; await peerConnectionRef.current.setRemoteDescription( new RTCSessionDescription(remoteRTCMessage.current) ); setCallStarted(true); }; const onICEcandidate = (payload) => { console.log("收到ICE候选"); const bigData = JSON.parse(payload.body); const data = JSON.parse(bigData.data); const message = data.rtcMessage; if (peerConnectionRef.current) { peerConnectionRef.current .addIceCandidate(new RTCIceCandidate({ candidate: message.candidate, sdpMLineIndex: message.label, sdpMid: message.id })) .then(() => console.log("ICE候选添加成功")) .catch(err => console.error("添加ICE候选失败:", err)); } }; // 渲染视频视图 return ( <View style={styles.container}> {/* 远程视频流 */} {remoteStream && ( <RTCView streamURL={remoteStream.toURL()} style={styles.remoteVideo} objectFit="cover" /> )} {/* 本地视频流(小窗) */} {localStream && ( <RTCView streamURL={localStream.toURL()} style={styles.localVideo} objectFit="cover" /> )} {/* 通话控制按钮 */} <View style={styles.controlBar}> <Button title="挂断" onPress={() => navigation.goBack()} /> </View> </View> ); }; export default CallPage;
额外解决建议
- 配置媒体权限:在
app.json中添加Expo权限配置:{ "expo": { "android": { "permissions": [ "android.permission.CAMERA", "android.permission.RECORD_AUDIO" ] } } } - 模拟器视频支持:部分Android模拟器存在摄像头模拟问题,建议使用真机测试,或在模拟器设置中启用虚拟摄像头。
- STUN/TURN服务器:公网环境下仅STUN服务器可能无法完成NAT穿越,建议添加TURN服务器配置。
- 流状态监听:可添加
localStream.onactive、remoteStream.onactive监听,排查流是否正常激活。
内容的提问来源于stack exchange,提问作者Stanislav
相关产品推荐
相关产品推荐

