WebRTC应用设置远程描述与创建应答报错排查及解决问询
WebRTC React应用远程描述设置与应答创建问题排查与解决
问题背景
开发基于WebRTC API的React应用,使用Socket.IO作为信令服务器,运行在http://localhost:3000。目前遇到设置远程描述与创建应答时报错的问题,仅本地流能正常展示,远程流无法加载。
相关代码
RoomPage组件
import React, { useCallback, useEffect, useState } from "react"; import { useSocket } from "../providers/Socket"; import { usePeer } from "../providers/Peer"; const RoomPage = () => { const { socket } = useSocket(); const { peer } = usePeer(); const { createOffer, createAnswer, setRemoteAns, sendStream, remoteStream, setRemoteStream, } = usePeer(); const [myStream, setMyStream] = useState(null); const [remoteEmailId, setRemoteEmailId] = useState(null); const handleNewUserJoined = useCallback( async (data) => { const { emailId } = data; console.log("New user joined room : ", emailId); const offer = await createOffer(); socket.emit("call-user", { emailId, offer }); setRemoteEmailId(emailId); }, [createOffer, socket] ); const handleIncomingCall = useCallback( async (data) => { const { from, offer } = data; console.log("Incoming call from ", from, offer); const ans = await createAnswer(offer); socket.emit("call-accepted", { emailId: from, ans }); setRemoteEmailId(from); }, [createAnswer, socket] ); const handleCallAccepted = useCallback( async (data) => { const { ans } = data; console.log("Call got accepted: ", ans); if (peer) { try { await setRemoteAns(ans); // Set the remote answer } catch (error) { console.error("Error setting remote description (answer):", error); } } }, [peer, setRemoteAns] ); const getUserMediaStream = useCallback(async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true, }); setMyStream(stream); setRemoteStream(remoteStream); } catch (error) { console.error("Error accessing user media:", error); } }, []); const handleNegotiation = useCallback(async () => { const localOffer = peer.localDescription; socket.emit("call-user", { emailId: remoteEmailId, offer: localOffer }); }, [peer.localDescription, remoteEmailId, socket]); useEffect(() => { socket.on("user-joined", handleNewUserJoined); socket.on("incoming-call", handleIncomingCall); socket.on("call-accepted", handleCallAccepted); return () => { socket.off("user-joined", handleNewUserJoined); socket.off("incoming-call", handleIncomingCall); socket.off("call-accepted", handleCallAccepted); }; }, []); useEffect(() => { peer.addEventListener("negotiationneeded", handleNegotiation); return () => { peer.removeEventListener("negotiationneeded", handleNegotiation); }; }, [handleNegotiation]); useEffect(() => { getUserMediaStream(); }, [getUserMediaStream]); const handleSendVideo = () => { console.log("Sending video stream..."); sendStream(myStream); }; return ( <div className="room-page-container"> <h1>Room Page</h1> <h4>You are connected to {remoteEmailId}</h4> <button onClick={handleSendVideo}>Send my Video</button> {myStream && ( <video autoPlay playsInline muted ref={(videoRef) => { if (videoRef) { videoRef.srcObject = myStream; } }} ></video> )} {remoteStream && ( <video autoPlay playsInline ref={(videoRef) => { if (videoRef) { videoRef.srcObject = remoteStream; } }} ></video> )} </div> ); }; export default RoomPage;
PeerProvider组件
import React, { useMemo, useState, useCallback, useEffect } from "react"; const PeerContext = React.createContext(null); export const usePeer = () => React.useContext(PeerContext); export const PeerProvider = (props) => { const [peer, setPeer] = useState(null); const [remoteStream, setRemoteStream] = useState(null); useEffect(() => { const initPeer = async () => { const rtcPeer = new RTCPeerConnection({ iceServers: [ { urls: [ "stun:stun.l.google.com:19302", "stun:global.stun.twilio.com:3478", ], }, ], }); setPeer(rtcPeer); }; initPeer(); }, []); const handleTrackEvent = useCallback((ev) => { const streams = ev.streams; setRemoteStream(streams[0]); }, []); useEffect(() => { if (peer) { peer.addEventListener("track", handleTrackEvent); return () => { peer.removeEventListener("track", handleTrackEvent); }; } }, [handleTrackEvent, peer]); const createOffer = useCallback(async () => { if (peer) { const offer = await peer.createOffer(); await peer.setLocalDescription(offer); return offer; } return null; }, [peer]); const createAnswer = async (offer) => { if (peer) { try { await peer.setRemoteDescription(offer); // Set the remote offer const answer = await peer.createAnswer(); await peer.setLocalDescription(answer); // Set the local answer return answer; } catch (error) { console.error("Error creating answer:", error); return null; } } return null; }; const setRemoteAns = async (ans) => { if (peer) { try { await peer.setRemoteDescription(ans); // Set the remote answer } catch (error) { console.error("Error setting remote description (answer):", error); } } }; const sendStream = async (stream) => { if (peer) { const tracks = stream.getTracks(); tracks.forEach((track) => { if (!peer.getSenders().some((sender) => sender.track === track)) { peer.addTrack(track, stream); } }); } }; return ( <PeerContext.Provider value={{ peer, createOffer, createAnswer, setRemoteAns, sendStream, remoteStream, setRemoteStream, }} > {props.children} </PeerContext.Provider> ); };
问题分析与解决
1. 报错原因分析
- 依赖项缺失与状态不一致:socket事件监听的
useEffect未添加回调函数作为依赖,导致回调更新后监听的仍是旧版本;createAnswer未用useCallback包裹,每次渲染都会生成新函数,引发状态不匹配。 - PeerConnection状态判断缺失:
negotiationneeded事件监听时未检查peer是否初始化完成,直接调用addEventListener会触发null报错。 - 媒体流添加时机错误:需要手动点击按钮才添加流,但WebRTC协商在流添加前就已触发,导致SDP不完整,协商失败。
- 无效状态更新:
getUserMediaStream中调用setRemoteStream(remoteStream),将远程流设置为初始null,覆盖后续正确的远程流数据。
2. 具体解决步骤
(1)修复依赖项与回调稳定性
- 更新socket事件监听的
useEffect,添加所有相关回调作为依赖:useEffect(() => { socket.on("user-joined", handleNewUserJoined); socket.on("incoming-call", handleIncomingCall); socket.on("call-accepted", handleCallAccepted); return () => { socket.off("user-joined", handleNewUserJoined); socket.off("incoming-call", handleIncomingCall); socket.off("call-accepted", handleCallAccepted); }; }, [handleNewUserJoined, handleIncomingCall, handleCallAccepted, socket]); - 给
createAnswer添加useCallback包裹,保证函数引用稳定:const createAnswer = useCallback(async (offer) => { if (peer) { try { await peer.setRemoteDescription(offer); const answer = await peer.createAnswer(); await peer.setLocalDescription(answer); return answer; } catch (error) { console.error("Error creating answer:", error); return null; } } return null; }, [peer]); - 修复
negotiationneeded事件监听,先判断peer是否存在:useEffect(() => { if (!peer) return; peer.addEventListener("negotiationneeded", handleNegotiation); return () => { peer.removeEventListener("negotiationneeded", handleNegotiation); }; }, [handleNegotiation, peer]);
(2)优化媒体流逻辑
- 修改
getUserMediaStream,获取流后自动添加到PeerConnection,移除手动按钮:const getUserMediaStream = useCallback(async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true, }); setMyStream(stream); sendStream(stream); // 自动添加流到PeerConnection } catch (error) { console.error("Error accessing user media:", error); } }, [sendStream]); - 移除页面中的
handleSendVideo按钮及相关代码。
(3)修复无效状态更新
- 删除
getUserMediaStream中的setRemoteStream(remoteStream);代码行,避免错误覆盖远程流。
(4)优化协商逻辑
- 修改
handleNegotiation,重新创建offer而非复用旧的localDescription,确保SDP最新:const handleNegotiation = useCallback(async () => { if (!peer || !remoteEmailId) return; const offer = await createOffer(); socket.emit("call-user", { emailId: remoteEmailId, offer }); }, [peer, remoteEmailId, createOffer, socket]);
(5)添加PeerConnection状态判断
- 在
handleNewUserJoined等核心函数中添加peer存在性检查,避免在Peer未初始化时执行逻辑:const handleNewUserJoined = useCallback( async (data) => { const { emailId } = data; if (!peer) return; console.log("New user joined room : ", emailId); const offer = await createOffer(); socket.emit("call-user", { emailId, offer }); setRemoteEmailId(emailId); }, [createOffer, socket, peer] );
内容的提问来源于stack exchange,提问作者Noor Fatima
相关产品推荐
相关产品推荐

