PeerJS生成过多Client ID致视频异常,如何解决?
PeerJS生成大量Client ID及重复视频流的修复方案
在React音视频项目中,用户连接房间时,PeerJS终端会输出大量重复的Client ID,同时页面上出现多个重复的视频流,严重影响正常使用。终端输出示例:
Client connected: 2eaceef7-f9ed-447e-9e2b-6317df92ecb8
Client connected: f7562550-cb63-42c4-b372-325ae4e7e086
Client connected: 54a49645-1fd0-4907-945c-6c658424c870
...
代码问题分析
- Peer实例重复创建:在组件顶层直接
new Peer(),组件每次重渲染都会生成新的Peer实例,每个实例都会连接到服务器并生成新的Client ID,导致大量重复连接。 - 事件监听未清理:useEffect中绑定的Socket和Peer事件没有在组件卸载时移除,加上组件重渲染,会导致事件多次绑定,重复触发连接逻辑,生成多个视频。
- call事件处理错误:在
peer.on('call')回调里重复调用call.answer(userVideoStream),应答呼叫应该只用本地流调用一次,重复调用会导致异常。 - Peers状态管理错误:
setPeers({id})会直接覆盖整个peers对象,而不是添加新的Peer连接,导致无法正确管理已连接的用户。
修复后的完整代码
import React, { useState, useEffect, useRef, useCallback } from "react"; import { CopyToClipboard } from "react-copy-to-clipboard"; import { Peer } from 'peerjs'; import { useParams } from 'react-router'; import "./Chaterio.css"; import io from "socket.io-client"; import styled from "styled-components"; const socket = io.connect("http://localhost:9000"); const VideoCall = () => { const [localStream, setLocalStream] = useState(null); const [peers, setPeers] = useState({}); const videoGridRef = useRef(null); const myVideo = useRef(); const peerRef = useRef(null); // 用ref存储Peer实例,避免重复创建 const { roomId } = useParams(); useEffect(() => { // 创建单个Peer实例 peerRef.current = new Peer(undefined, { host: '/', port: 9001, concurrency: 1, }); const peer = peerRef.current; // 处理Peer打开事件 const handlePeerOpen = (id) => { socket.emit("join-room", roomId, id); console.log("My Peer ID: ", id); }; // 处理来电事件 const handleCall = (call) => { call.answer(localStream); // 只用本地流应答一次 const video = document.createElement('video'); call.on('stream', (userVideoStream) => { addVideoStream(video, userVideoStream); }); call.on('close', () => { video.remove(); }); // 更新peers状态 setPeers(prev => ({ ...prev, [call.peer]: call })); }; // 处理新用户连接事件 const handleUserConnected = useCallback((userId) => { if (!localStream) return; connectToNewUser(userId, localStream); }, [localStream]); // 处理用户断开事件 const handleUserDisconnected = useCallback((userId) => { console.log("User disconnected: ", userId); if (peers[userId]) { peers[userId].close(); setPeers(prev => { const newPeers = { ...prev }; delete newPeers[userId]; return newPeers; }); } }, [peers]); // 绑定事件 peer.on("open", handlePeerOpen); peer.on('call', handleCall); socket.on("user-connected", handleUserConnected); socket.on("user-disconnected", handleUserDisconnected); // 获取本地媒体流 navigator.mediaDevices.getUserMedia({ video: true, audio: true }) .then((stream) => { setLocalStream(stream); if (myVideo.current) { myVideo.current.srcObject = stream; myVideo.current.muted = true; } }) .catch((error) => { console.error("获取媒体流失败: ", error); }); // 组件卸载时清理资源 return () => { peer.destroy(); // 销毁Peer实例 socket.off("user-connected", handleUserConnected); socket.off("user-disconnected", handleUserDisconnected); peer.off("open", handlePeerOpen); peer.off("call", handleCall); // 关闭本地流 if (localStream) { localStream.getTracks().forEach(track => track.stop()); } }; }, [roomId]); const connectToNewUser = useCallback((userId, stream) => { const peer = peerRef.current; if (!peer) return; const call = peer.call(userId, stream); const video = document.createElement('video'); call.on('stream', (remoteStream) => { addVideoStream(video, remoteStream); }); call.on('close', () => { video.remove(); setPeers(prev => { const newPeers = { ...prev }; delete newPeers[userId]; return newPeers; }); }); setPeers(prev => ({ ...prev, [userId]: call })); }, []); const addVideoStream = useCallback((video, stream) => { video.srcObject = stream; video.addEventListener("loadedmetadata", () => { video.play(); }); if (videoGridRef.current) { // 检查是否已存在该流的视频元素,避免重复添加 const existingVideo = Array.from(videoGridRef.current.children).find(v => v.srcObject === stream); if (!existingVideo) { videoGridRef.current.appendChild(video); } } }, []); return ( <> <OuterContainer> <Header><AlinkHeader href="/">Chaterio</AlinkHeader></Header> <Container> <CopyToClipboard text={roomId} style={{ marginBottom: ".5rem" }}> <Button variant="contained" color="primary" > Copy Room Id </Button> </CopyToClipboard> </Container> <VideoContainer> <VideoDrag > {localStream && <video id="myVideo" draggable="true" playsInline muted ref={myVideo} autoPlay width={500} height={500} />} </VideoDrag> <div id="video-grid" ref={videoGridRef} /> </VideoContainer> <SectionOuterButtons> <SectionInnerButtons> <a href="/"><RoomButton>Leave Room</RoomButton></a> <a href="/room"><RoomButton>Join Room</RoomButton></a> </SectionInnerButtons> </SectionOuterButtons> </OuterContainer> </> ); }; export default VideoCall;
关键修复点说明
- 用useRef存储Peer实例:确保组件生命周期内只创建一个Peer实例,避免重复连接生成大量Client ID。
- 清理事件监听和资源:在useEffect的返回函数中销毁Peer实例、移除Socket和Peer的事件监听,关闭本地媒体流,防止内存泄漏和重复触发。
- 修正call事件逻辑:移除重复的
call.answer调用,只用本地流应答一次来电。 - 正确维护peers状态:更新peers时使用扩展运算符保留原有连接,删除断开用户的连接,确保状态准确。
- 添加重复视频检查:在
addVideoStream中检查是否已存在对应流的视频元素,避免重复添加。
内容的提问来源于stack exchange,提问作者David James
相关产品推荐
相关产品推荐

