实时群组视频聊天异常:多用户启动报视频源错误及屏幕共享失效求助
实时群组视频聊天问题修复
问题现象
- 首个用户打开页面功能正常,第二个用户打开时触发错误:
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"错误
- 修正媒体流请求时机:将
roomId和userId加入useEffect依赖,确保用户填写完必要信息后才请求摄像头/麦克风,避免无效的媒体设备调用。 - 区分Peer连接角色:
connectToNewUser中动态设置isInitiator,仅主动发起连接的一方作为发起者,防止双方同时发起连接导致设备冲突。 - 添加资源清理逻辑:在组件卸载或依赖变化时,停止本地媒体流、销毁Peer连接、移除Socket事件监听,避免设备占用和内存泄漏。
解决屏幕共享无法广播问题
- 替换无效的流传输逻辑:移除通过Socket直接发送
MediaStream的错误代码,改用replaceTrack替换已建立连接中的视频轨道,实现屏幕流的广播。 - 覆盖所有连接节点:屏幕共享时遍历所有已连接的Peer,替换每个连接的视频轨道,确保所有用户都能看到屏幕流。
- 添加自动切换逻辑:监听屏幕流的
onended事件,在用户停止共享时自动切换回摄像头流,并同步更新所有Peer连接的视频轨道。
内容的提问来源于stack exchange,提问作者Hadi Ranjbar
相关产品推荐
相关产品推荐

