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

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');
});

关键修复点说明

  1. 状态流转校验:在处理offer前,检查RTCPeerConnection的signalingState是否为stable,确保只有在正确状态下执行后续操作
  2. 实例管理:用React的useRef存储Peer和Socket实例,避免组件重渲染导致实例被重新创建,触发重复的信令操作
  3. 信令顺序:严格按照「设置远程offer → 创建answer → 设置本地描述 → 发送answer」的顺序执行,确保RTCPeerConnection状态正常流转

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 22:30:53