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
相关产品推荐
相关产品推荐

