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

实时群组视频聊天异常:多用户启动报视频源错误及屏幕共享失效求助

实时群组视频聊天问题修复

问题现象

  • 首个用户打开页面功能正常,第二个用户打开时触发错误:Uncaught (in promise) DOMException: Could not start video source
  • 屏幕共享无法向其他用户广播

修复后代码

const socket = io('http://localhost:3001');

function App() {
  const [roomId, setRoomId] = useState('');
  const [userId, setUserId] = useState('');
  const [messages, setMessages] = useState([]);
  const [peers, setPeers] = useState([]);
  const userVideoRef = useRef();
  const peersRef = useRef([]);
  const localStreamRef = useRef(null); // 保存本地媒体流引用

  useEffect(() => {
    // 仅当房间ID和用户ID都填写后才执行媒体流请求和房间加入逻辑
    if (!roomId || !userId) return;

    navigator.mediaDevices.getUserMedia({ video: true, audio: true })
      .then((stream) => {
        localStreamRef.current = stream;
        userVideoRef.current.srcObject = stream;

        socket.emit('join-room', roomId, userId);

        // 绑定socket事件监听
        const handleUserConnected = (newUserId) => {
          connectToNewUser(newUserId, stream, false); // 接收方不作为发起者
        };

        const handleUserDisconnected = (disconnectedUserId) => {
          const peer = peersRef.current.find((p) => p.userId === disconnectedUserId);
          if (peer) peer.peer.destroy();
          setPeers(prev => prev.filter(p => p.userId !== disconnectedUserId));
        };

        const handleReceiveMessage = ({ userId, message }) => {
          setMessages(prev => [...prev, `${userId}: ${message}`]);
        };

        const handleOffer = (room, fromUserId, offer) => {
          if (room === roomId) connectToNewUser(fromUserId, localStreamRef.current, true);
        };

        socket.on('user-connected', handleUserConnected);
        socket.on('user-disconnected', handleUserDisconnected);
        socket.on('receive-message', handleReceiveMessage);
        socket.on('offer', handleOffer);

        // 清理函数:释放资源、移除事件监听
        return () => {
          socket.off('user-connected', handleUserConnected);
          socket.off('user-disconnected', handleUserDisconnected);
          socket.off('receive-message', handleReceiveMessage);
          socket.off('offer', handleOffer);
          
          if (localStreamRef.current) {
            localStreamRef.current.getTracks().forEach(track => track.stop());
          }
          peersRef.current.forEach(p => p.peer.destroy());
        };
      })
      .catch(err => console.error('获取媒体流失败:', err));

  }, [roomId, userId]); // 依赖房间ID和用户ID,值变化时重新执行

  const connectToNewUser = (newUserId, userStream, isInitiator = false) => {
    const peer = new Peer({
      initiator: isInitiator,
      trickle: false,
      stream: userStream,
    });
  
    peer.on('signal', (signalData) => {
      socket.emit(isInitiator ? 'offer' : 'answer', roomId, newUserId, signalData);
    });
  
    peer.on('stream', (userVideoStream) => {
      // 避免重复添加相同用户的流
      setPeers(prev => {
        if (!prev.find(p => p.userId === newUserId)) {
          return [...prev, { userId: newUserId, stream: userVideoStream }];
        }
        return prev;
      });
    });
  
    socket.on('answer', (room, fromUserId, answer) => {
      if (room === roomId && fromUserId === newUserId) {
        peer.signal(answer);
      }
    });
  
    socket.on('receive-ice-candidate', (iceCandidateUserId, iceCandidate) => {
      if (iceCandidateUserId === newUserId) {
        peer.signal(iceCandidate);
      }
    });
  
    peersRef.current.push({ userId: newUserId, peer });
  };
  

  const sendMessage = (message) => {
    socket.emit('send-message', roomId, userId, message);
    setMessages(prev => [...prev, `You: ${message}`]);
  };

  const shareScreen = async () => {
    try {
      const screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
      localStreamRef.current = screenStream;
      userVideoRef.current.srcObject = screenStream;

      // 替换所有已连接peer的视频轨道
      peersRef.current.forEach(({ peer }) => {
        const videoSender = peer._pc.getSenders().find(s => s.track.kind === 'video');
        if (videoSender) {
          videoSender.replaceTrack(screenStream.getVideoTracks()[0]);
        }
      });

      // 监听屏幕共享停止事件,自动切换回摄像头
      screenStream.getVideoTracks()[0].onended = async () => {
        const cameraStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
        localStreamRef.current = cameraStream;
        userVideoRef.current.srcObject = cameraStream;
        
        peersRef.current.forEach(({ peer }) => {
          const videoSender = peer._pc.getSenders().find(s => s.track.kind === 'video');
          if (videoSender) {
            videoSender.replaceTrack(cameraStream.getVideoTracks()[0]);
          }
        });
      };
    } catch (error) {
      console.error('屏幕共享失败:', error.message);
    }
  };
  
  
  return (
    <div>
      <div>
        <label>Room ID:</label>
        <input
          type="text"
          value={roomId}
          onChange={(e) => setRoomId(e.target.value)}
          placeholder="Enter Room ID"
        />
      </div>
      <div>
        <label>User ID:</label>
        <input
          type="text"
          value={userId}
          onChange={(e) => setUserId(e.target.value)}
          placeholder="Enter User ID"
        />
      </div>
      <video ref={userVideoRef} autoPlay muted style={{ width: '200px' }} />
      <div>
        <h3>Peers:</h3>
        <div style={{ display: 'flex', flexWrap: 'wrap' }}>
          {peers.map((peer) => (
            <video key={peer.userId} autoPlay playsInline style={{ width: '200px', margin: '5px' }} ref={(el) => el && (el.srcObject = peer.stream)} />
          ))}
        </div>
      </div>
      <div>
        <button onClick={shareScreen}>Share Screen</button>
      </div>
      <div>
        <h3>Chat:</h3>
        <ul>
          {messages.map((message, index) => (
            <li key={index}>{message}</li>
          ))}
        </ul>
        <input
          type="text"
          placeholder="Type your message..."
          onKeyPress={(e) => e.key === 'Enter' && sendMessage(e.target.value)}
        />
      </div>
    </div>
  );
}

export default App;

关键修复说明

解决"Could not start video source"错误

  1. 修正媒体流请求时机:将roomId和userId加入useEffect依赖,确保用户填写完必要信息后才请求摄像头/麦克风,避免无效的媒体设备调用。
  2. 区分Peer连接角色:connectToNewUser中动态设置isInitiator,仅主动发起连接的一方作为发起者,防止双方同时发起连接导致设备冲突。
  3. 添加资源清理逻辑:在组件卸载或依赖变化时,停止本地媒体流、销毁Peer连接、移除Socket事件监听,避免设备占用和内存泄漏。

解决屏幕共享无法广播问题

  1. 替换无效的流传输逻辑:移除通过Socket直接发送MediaStream的错误代码,改用replaceTrack替换已建立连接中的视频轨道,实现屏幕流的广播。
  2. 覆盖所有连接节点:屏幕共享时遍历所有已连接的Peer,替换每个连接的视频轨道,确保所有用户都能看到屏幕流。
  3. 添加自动切换逻辑:监听屏幕流的onended事件,在用户停止共享时自动切换回摄像头流,并同步更新所有Peer连接的视频轨道。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 22:06:05