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

simple-peer+React视频聊天应用部署后Socket功能异常排查

问题根因与修复方案

核心现象

你遇到的两个问题都是「本地环境无延迟、生产环境时序/组件用法问题被放大」导致的,和simple-peer、信令服务的核心逻辑无关:

  • 最先进入房间的用户无法感知新用户加入
  • 后进入房间的用户页面显示两个自身画面

根因拆解

1. 老用户感知不到新用户加入的原因

你把socket.on("user joined")、socket.on("receiving-returned-signal")两个核心信令监听,写在了getUserMedia的异步成功回调里。
本地开发环境一般提前给了localhost摄像头权限,getUserMedia返回速度极快,监听总能在新用户的信令到达前注册完成,不会丢事件。但生产环境下,首次访问需要用户手动点击授权摄像头、设备初始化摄像头也需要耗时,很容易出现「新用户已经加入房间、服务端已经把user joined事件发过来了,老用户这边还没走完摄像头授权流程、根本没注册上对应监听」的情况,事件直接被丢弃,老用户自然收不到通知。

2. 新用户看到两个自身画面的原因

两个问题叠加导致:

  • 远端视频组件用错:你在渲染远端用户流的Video组件里用了react-webcam的Webcam组件,这个组件的内置逻辑就是自动调用getUserMedia拉取本地摄像头流渲染,哪怕你后续手动给它的srcObject赋值远端流,组件初始化、重渲染时都可能把值覆盖回本地流。
  • simple-peer的默认行为:创建Peer实例时如果传入了本地stream参数,实例初始化阶段会立刻触发一次stream事件,返回值就是你传入的本地流,等P2P连接建立完成拿到远端流后,才会第二次触发stream事件返回远端流。本地环境下localhost打洞、信令交换几乎无延迟,远端流很快就到,第二次触发事件时会把本地流覆盖,所以看起来正常;生产环境下信令交换、P2P打洞耗时更长,很容易停留在第一次触发的本地流画面上。

修复方案

第一步:调整信令监听注册时机

把socket事件监听从getUserMedia的回调里挪出来,组件挂载时就注册,本地流用ref单独存储,创建peer时从ref取流即可。

第二步:远端视频换用原生video标签

Webcam组件仅用于本地流采集,远端流播放直接用原生<video>标签,不要用封装好的Webcam组件。

第三步:过滤本地流事件

在peer的stream事件回调里加判断,和本地流id一致的流直接忽略,避免把本地流挂到远端视频容器上。

其他优化点

  • 不要用数组index作为peer列表渲染的key,改用每个peer对应的用户唯一ID作为key,避免重渲染时组件状态错乱。
  • 完善useEffect清理逻辑:组件卸载时遍历所有peer实例调用destroy(),停止本地流的所有音视频track,再移除socket监听,避免内存泄漏、摄像头指示灯常亮。
  • 给peer实例添加error、close事件监听,生产环境可以快速定位打洞失败、连接断开的问题。

修复后核心代码参考

客户端核心逻辑

// 远端视频组件,改用原生video标签
const Video = (props) => {
  const anotherUserRef = useRef(null);
  useEffect(() => {
    const handleStream = (stream) => {
      // 过滤掉本地流,避免显示自己
      if (stream.id === localStreamRef.current?.id) return;
      anotherUserRef.current.srcObject = stream;
    };
    props.peer.on("stream", handleStream);
    return () => {
      props.peer.off("stream", handleStream);
    };
  }, [props.peer]);

  return (
    <video
      className="anotherUser"
      playsInline
      autoPlay
      ref={anotherUserRef}
    />
  );
};

// 组件内
const localStreamRef = useRef(null);
const peersRef = useRef([]);
const [peers, setPeers] = useState([]);

useEffect(() => {
  // 提前注册socket监听,不要等getUserMedia完成
  const handleUserJoined = (payload) => {
    if (!localStreamRef.current) return; // 还没拿到本地流就先暂存,不要提前创建peer
    const peer = addPeer(payload.signal, payload.callerID, localStreamRef.current);
    peersRef.current.push({
      peerID: payload.callerID,
      peer,
    });
    setPeers((users) => [...users, { peerID: payload.callerID, peer }]);
  };

  const handleReturnedSignal = (payload) => {
    const item = peersRef.current.find((p) => p.peerID === payload.id);
    item?.peer.signal(payload.signal);
  };

  socket.on("user joined", handleUserJoined);
  socket.on("receiving-returned-signal", handleReturnedSignal);

  // 再处理媒体流申请
  navigator.mediaDevices
    .getUserMedia({
      video: videoConstraints,
      audio: true,
    })
    .then((stream) => {
      localStreamRef.current = stream;
      userVideo.current.srcObject = stream;

      // 拿到流之后再给已经在房间的用户创建连接
      const peers = [];
      userInfo.forEach((user) => {
        const peer = createPeer(user, socket.id, stream);
        peersRef.current.push({
          peerID: user,
          peer,
        });
        peers.push({ peerID: user, peer });
      });
      setPeers(peers);
    });

  return () => {
    // 清理所有peer连接
    peersRef.current.forEach(({ peer }) => peer.destroy());
    peersRef.current = [];
    // 停止本地流
    if (localStreamRef.current) {
      localStreamRef.current.getTracks().forEach(track => track.stop());
      localStreamRef.current = null;
    }
    // 移除监听
    socket.off("user joined", handleUserJoined);
    socket.off("receiving-returned-signal", handleReturnedSignal);
    socket.off(SOCKET.USER);
  };
}, []);

// 渲染部分,用peerID当key
return (
  <>
    <Webcam className="user" ref={userVideo} autoPlay playsInline />
    {peers.map(({ peer, peerID }) => <Video key={peerID} peer={peer} />)}
  </>
);

服务端逻辑

原有信令转发逻辑没有核心问题,可以保留,建议加个异常判断,避免找不到对应用户时报错:

socket.on("sending signal", (payload) => {
  if (!payload.userToSignal) return;
  io.to(payload.userToSignal).emit("user joined", {
    signal: payload.signal,
    callerID: payload.callerID,
  });
});

socket.on("returning signal", (payload) => {
  if (!payload.callerID) return;
  io.to(payload.callerID).emit("receiving-returned-signal", {
    signal: payload.signal,
    id: socket.id,
  });
});

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 11:30:57