React JS与expo-cli版React Native跨端视频通话方案咨询
可行性结论
React JS Web端 + Expo 搭建的 React Native 端实现跨端视频通话完全可落地,核心基于WebRTC协议实现,两端均有成熟的适配库支持,不需要替换现有技术栈。
原生WebRTC实现方案(无强绑定第三方付费服务)
这个方案自由度最高,所有逻辑可控,核心分为三部分:信令服务、Web端实现、Expo RN端实现。
前置说明
WebRTC本身不需要服务端中转音视频流,但需要一个轻量信令服务做连接前的信息交换(交换SDP协商信息、ICE网络候选地址),信令服务不处理音视频数据,压力极小。
1. 最简信令服务实现
你可以用Node.js搭一个仅几十行代码的WebSocket服务做信令,双人通话场景下直接做消息广播即可:
// 信令服务代码,基于ws库,先执行 npm i ws 安装依赖 const { WebSocketServer } = require('ws'); const wss = new WebSocketServer({ port: 8080 }); const connectedClients = new Set(); wss.on('connection', (client) => { connectedClients.add(client); // 收到任意客户端的消息,直接转发给其他所有在线客户端 client.on('message', (payload) => { connectedClients.forEach(peer => { if (peer !== client && peer.readyState === 1) { peer.send(payload.toString()); } }); }); client.on('close', () => connectedClients.delete(client)); });
本地测试直接node 文件名.js启动即可,生产环境记得加wss加密、简单的房间逻辑区分不同通话组。
2. React Web端实现
Web端直接用浏览器原生WebRTC API即可,不需要额外安装重型依赖,核心代码如下:
import { useEffect, useRef } from 'react'; export default function VideoCall() { const localVideoRef = useRef(null); const remoteVideoRef = useRef(null); const peerConnection = useRef(null); const ws = useRef(new WebSocket('ws://127.0.0.1:8080')); // 填你的信令服务地址 useEffect(() => { const initCall = async () => { // 获取本地摄像头、麦克风权限 const localStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user' }, audio: true }); localVideoRef.current.srcObject = localStream; // 初始化点对点连接,配置公共STUN服务器做网络探测 peerConnection.current = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }); // 将本地音视频轨加入连接 localStream.getTracks().forEach(track => { peerConnection.current.addTrack(track, localStream); }); // 收到对端媒体流时,绑定到远端视频元素 peerConnection.current.ontrack = (event) => { remoteVideoRef.current.srcObject = event.streams[0]; }; // 收集到本地网络候选地址时,通过信令发给对端 peerConnection.current.onicecandidate = (event) => { if (event.candidate) { ws.current.send(JSON.stringify({ type: 'ice-candidate', payload: event.candidate })); } }; }; initCall(); // 监听信令消息,处理对端发来的协商信息 ws.current.onmessage = async (event) => { const message = JSON.parse(event.data); switch (message.type) { case 'offer': await peerConnection.current.setRemoteDescription(new RTCSessionDescription(message.payload)); const answer = await peerConnection.current.createAnswer(); await peerConnection.current.setLocalDescription(answer); ws.current.send(JSON.stringify({ type: 'answer', payload: answer })); break; case 'answer': await peerConnection.current.setRemoteDescription(new RTCSessionDescription(message.payload)); break; case 'ice-candidate': await peerConnection.current.addIceCandidate(new RTCIceCandidate(message.payload)); break; } }; }, []); // 主动发起通话 const handleStartCall = async () => { const offer = await peerConnection.current.createOffer(); await peerConnection.current.setLocalDescription(offer); ws.current.send(JSON.stringify({ type: 'offer', payload: offer })); }; return ( <div className="video-container"> <video ref={localVideoRef} autoPlay muted style={{ width: 160, height: 240 }} /> <video ref={remoteVideoRef} autoPlay style={{ width: 480, height: 320 }} /> <button onClick={handleStartCall}>发起通话</button> </div> ); }
3. Expo React Native端实现
前置配置
Expo Go不包含WebRTC相关原生模块,必须打包自定义开发客户端才能运行,步骤如下:
- 安装依赖:
npx expo install react-native-webrtc @config-plugins/react-native-webrtc - 修改
app.json,添加插件配置和权限声明:
{ "expo": { "plugins": ["@config-plugins/react-native-webrtc"], "ios": { "infoPlist": { "NSCameraUsageDescription": "需要摄像头权限进行视频通话", "NSMicrophoneUsageDescription": "需要麦克风权限进行视频通话" } }, "android": { "permissions": ["CAMERA", "RECORD_AUDIO"] } } }
- 执行
npx expo prebuild生成原生工程,再执行npx expo run:android或npx expo run:ios启动自定义开发客户端即可。
RN端核心代码
信令处理逻辑和Web端完全一致,仅媒体采集、视频渲染用RN端适配的API:
import { useEffect, useRef, useState } from 'react'; import { View, Button, StyleSheet } from 'react-native'; import { mediaDevices, RTCPeerConnection, RTCView } from 'react-native-webrtc'; export default function VideoCallScreen() { const [localStream, setLocalStream] = useState(null); const [remoteStream, setRemoteStream] = useState(null); const peerConnection = useRef(null); const ws = useRef(new WebSocket('ws://127.0.0.1:8080')); // 和Web端填同一个信令地址 useEffect(() => { const initCall = async () => { // 申请权限并获取本地音视频流 const stream = await mediaDevices.getUserMedia({ video: { facingMode: 'user' }, audio: true }); setLocalStream(stream); // 初始化点对点连接,iceServers配置必须和Web端完全一致 peerConnection.current = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }); stream.getTracks().forEach(track => { peerConnection.current.addTrack(track, stream); }); peerConnection.current.ontrack = (event) => { setRemoteStream(event.streams[0]); }; peerConnection.current.onicecandidate = (event) => { if (event.candidate) { ws.current.send(JSON.stringify({ type: 'ice-candidate', payload: event.candidate })); } }; }; initCall(); // 信令处理逻辑和Web端完全相同 ws.current.onmessage = async (event) => { const message = JSON.parse(event.data); switch (message.type) { case 'offer': await peerConnection.current.setRemoteDescription(new RTCSessionDescription(message.payload)); const answer = await peerConnection.current.createAnswer(); await peerConnection.current.setLocalDescription(answer); ws.current.send(JSON.stringify({ type: 'answer', payload: answer })); break; case 'answer': await peerConnection.current.setRemoteDescription(new RTCSessionDescription(message.payload)); break; case 'ice-candidate': await peerConnection.current.addIceCandidate(new RTCIceCandidate(message.payload)); break; } }; }, []); const handleStartCall = async () => { const offer = await peerConnection.current.createOffer(); await peerConnection.current.setLocalDescription(offer); ws.current.send(JSON.stringify({ type: 'offer', payload: offer })); }; return ( <View style={styles.container}> {localStream && <RTCView streamURL={localStream.toURL()} style={styles.localVideo} />} {remoteStream && <RTCView streamURL={remoteStream.toURL()} style={styles.remoteVideo} />} <Button title="发起通话" onPress={handleStartCall} /> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, paddingTop: 50 }, localVideo: { width: 120, height: 180, position: 'absolute', top: 60, right: 20, zIndex: 10 }, remoteVideo: { width: '100%', height: 300 } });
替代方案(降低开发成本)
如果不想自己处理WebRTC的NAT穿透、弱网优化、流控这些逻辑,可以选以下方案:
- 用成熟的商用RTC SDK:比如声网Agora、腾讯实时音视频TRTC,都提供官方的React Web SDK和Expo适配的React Native SDK,按文档接入最快1小时就能跑通功能,免费额度足够小产品初期使用,超出后按用量付费
- 用封装好的WebRTC库:比如PeerJS,Web端直接引入npm包,RN端用
react-native-peerjs适配,自带公共信令服务器,测试场景不用自己搭服务,生产环境可以自己部署PeerServer做信令
常见踩坑提醒
- 不要尝试在Expo Go里调试WebRTC相关功能,Expo Go没有打包对应原生模块,跑不起来,必须用自定义dev客户端
- 公共STUN服务器仅能用于本地测试,生产环境必须部署TURN服务器,不然跨内网的客户端大概率无法连通
- 两端RTCPeerConnection的iceServers配置必须完全一致,否则会出现协商失败、连不通的问题
- React Native端只能用
react-native-webrtc提供的RTCView组件渲染视频流,用普通Video组件无法播放
内容的提问来源于stack exchange,提问作者Antonio Rodríguez
相关产品推荐
相关产品推荐

