React+PeerJS直播应用开发遇setLocalDescription状态错误
解决React+Vite+PeerJS直播应用中
setLocalDescription状态错误问题 错误信息:
DOMException: Failed to execute 'setLocalDescription' on 'RTCPeerConnection': Failed to set local answer sdp: Called in wrong state: stable
我在React+Vite环境下用PeerJS开发直播应用时碰到这个问题,翻了一圈没找到针对React场景的解决方案,最后结合两种方案搞定了,下面是具体修复代码和思路。
核心修复逻辑
- 严格遵循RTCPeerConnection状态流转:只有在连接处于
have-remote-offer状态时,才创建并设置answer - 用React的
useRef管理Peer和RTCPeerConnection实例,避免组件重渲染导致实例丢失或重复操作 - 优化信令逻辑,防止重复发送/接收offer/answer触发错误状态
webRTC.ts(核心封装代码)
import Peer from 'peerjs'; export const createPeerConnection = (peerId: string, onStream: (stream: MediaStream) => void) => { const peer = new Peer(peerId, { host: '/', port: 3001, path: '/peerjs' }); let peerConnection: RTCPeerConnection | null = null; peer.on('open', (id) => { console.log('Peer ID:', id); }); peer.on('call', async (call) => { const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true }); // 仅在收到通话请求时创建连接,避免重复实例 if (!peerConnection) { peerConnection = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }); // 添加本地媒体轨道 stream.getTracks().forEach(track => { peerConnection?.addTrack(track, stream); }); // 监听远程流 peerConnection.ontrack = (event) => { onStream(event.streams[0]); }; // 处理ICE候选 peerConnection.onicecandidate = (event) => { if (event.candidate) { call.send(event.candidate); } }; } // 处理信令数据 call.on('data', async (data) => { if (!peerConnection) return; if (data.type === 'offer') { // 先设置远程offer,再创建answer await peerConnection.setRemoteDescription(new RTCSessionDescription(data)); const answer = await peerConnection.createAnswer(); // 先设置本地描述,再发送answer await peerConnection.setLocalDescription(answer); call.send(peerConnection.localDescription); } else if (data.type === 'iceCandidate') { await peerConnection.addIceCandidate(new RTCIceCandidate(data)); } }); call.answer(stream); call.on('stream', onStream); }); return peer; };
liveLecture.tsx(React组件)
import { useEffect, useRef, useState } from 'react'; import { createPeerConnection } from './webRTC'; import io from 'socket.io-client'; const LiveLecture = () => { const [remoteStream, setRemoteStream] = useState<MediaStream | null>(null); // 用ref存Peer和Socket实例,避免重渲染重置 const peerRef = useRef<Peer | null>(null); const socketRef = useRef<SocketIOClient.Socket | null>(null); useEffect(() => { socketRef.current = io('http://localhost:3001'); // 初始化Peer连接 peerRef.current = createPeerConnection(`user-${Date.now()}`, (stream) => { setRemoteStream(stream); }); // 监听Socket转发的offer socketRef.current?.on('offer', async (offerData) => { const peerConn = peerRef.current?._connections[offerData.peerId]?.peerConnection; if (!peerConn || peerConn.signalingState !== 'stable') return; await peerConn.setRemoteDescription(new RTCSessionDescription(offerData.offer)); const answer = await peerConn.createAnswer(); await peerConn.setLocalDescription(answer); socketRef.current?.emit('answer', { peerId: offerData.peerId, answer }); }); // 清理函数 return () => { peerRef.current?.destroy(); socketRef.current?.disconnect(); }; }, []); return ( <div className="live-container"> <h2>直播画面</h2> {remoteStream && ( <video autoPlay playsInline srcObject={remoteStream} style={{ width: '100%', maxWidth: '800px', borderRadius: '8px' }} /> )} </div> ); }; export default LiveLecture;
Socket.io服务端代码
const express = require('express'); const http = require('http'); const { Server } = require('socket.io'); const { ExpressPeerServer } = require('peer'); const app = express(); const server = http.createServer(app); const io = new Server(server, { cors: { origin: '*' } }); // 挂载Peer服务器 const peerServer = ExpressPeerServer(server, { debug: true }); app.use('/peerjs', peerServer); io.on('connection', (socket) => { console.log('用户连接:', socket.id); // 转发offer给目标Peer socket.on('offer', (data) => { socket.to(data.peerId).emit('offer', data); }); // 转发answer给目标Peer socket.on('answer', (data) => { socket.to(data.peerId).emit('answer', data); }); socket.on('disconnect', () => { console.log('用户断开:', socket.id); }); }); server.listen(3001, () => { console.log('服务运行在端口3001'); });
关键修复点说明
- 状态流转校验:在处理offer前,检查RTCPeerConnection的
signalingState是否为stable,确保只有在正确状态下执行后续操作 - 实例管理:用React的
useRef存储Peer和Socket实例,避免组件重渲染导致实例被重新创建,触发重复的信令操作 - 信令顺序:严格按照「设置远程offer → 创建answer → 设置本地描述 → 发送answer」的顺序执行,确保RTCPeerConnection状态正常流转
内容的提问来源于stack exchange,提问作者Circuit Planet
相关产品推荐
相关产品推荐

