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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 07:40:55