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

React Native Expo中WebRTC视频通话连接正常但视频流异常求助

React Native Expo WebRTC视频流问题修复方案

环境信息

  • 依赖版本:
    • @config-plugins/react-native-webrtc": "^7.0.0
    • react-native-webrtc": "^111.0.3
  • 测试环境:Android模拟器(API level 34,镜像:system-images/android-34/google_apis/x86_x64)

当前Peer连接逻辑正常,但视频流无法正常显示,以下是问题排查和修复方案:

核心问题分析

  1. 重复获取本地媒体流:useEffect中的initLocalStream和createPeerConnection都调用了getUserMedia,导致重复请求媒体资源,引发流实例冲突。
  2. 缺少RTCView渲染组件:原代码未包含RTCView的渲染逻辑,这是WebRTC视频流在React Native中显示的必要组件。
  3. 媒体权限配置缺失:Expo环境下需确保已正确配置相机和麦克风权限。
  4. 远程流处理不严谨: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;

额外解决建议

  1. 配置媒体权限:在app.json中添加Expo权限配置:
    {
      "expo": {
        "android": {
          "permissions": [
            "android.permission.CAMERA",
            "android.permission.RECORD_AUDIO"
          ]
        }
      }
    }
    
  2. 模拟器视频支持:部分Android模拟器存在摄像头模拟问题,建议使用真机测试,或在模拟器设置中启用虚拟摄像头。
  3. STUN/TURN服务器:公网环境下仅STUN服务器可能无法完成NAT穿越,建议添加TURN服务器配置。
  4. 流状态监听:可添加localStream.onactive、remoteStream.onactive监听,排查流是否正常激活。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 05:13:13