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

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
...

代码问题分析

  1. Peer实例重复创建:在组件顶层直接new Peer(),组件每次重渲染都会生成新的Peer实例,每个实例都会连接到服务器并生成新的Client ID,导致大量重复连接。
  2. 事件监听未清理:useEffect中绑定的Socket和Peer事件没有在组件卸载时移除,加上组件重渲染,会导致事件多次绑定,重复触发连接逻辑,生成多个视频。
  3. call事件处理错误:在peer.on('call')回调里重复调用call.answer(userVideoStream),应答呼叫应该只用本地流调用一次,重复调用会导致异常。
  4. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 03:55:02