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

React+WebRTC视频通话应用ontrack事件未触发,无法传输视频轨道

React + WebRTC + Socket.io 视频通话:ontrack事件未触发问题排查与修复

核心问题分析与修复步骤

1. peer变量未用React状态管理,实例引用丢失

直接用var peer声明在组件内部,React组件重渲染时该变量会被重置,后续事件监听或操作的可能不是同一个RTCPeerConnection实例。

修复:将peer改为React状态:

const [peer, setPeer] = useState(null);

创建RTCPeerConnection时更新状态:

const newPeer = new RTCPeerConnection(configuration);
setPeer(newPeer);

2. ontrack事件绑定时机过晚

你在connection-complete回调里才绑定track事件,但此时对方可能已经发送了媒体轨道,事件已触发过,自然监听不到。

修复:创建RTCPeerConnection后立即绑定track事件,发起方和应答方都要执行:

const newPeer = new RTCPeerConnection(configuration);
// 立刻绑定track事件
newPeer.addEventListener('track', (event) => {
  const [remoteStream] = event.streams;
  setremotestream(remoteStream);
});
setPeer(newPeer);

3. 缺失ICE候选交换逻辑

代码仅打印ICE候选,未通过Socket.io发送给对方,对方无法获取你的网络信息,P2P连接无法建立,媒体轨道无法传输。

修复:

  • 发起方和应答方创建peer后,监听icecandidate事件并发送候选:
newPeer.addEventListener('icecandidate', (event) => {
  if (event.candidate) {
    socket.emit('send-ice-candidate', event.candidate);
  }
});
  • 监听对方发送的ICE候选并添加到本地peer:
// 在useEffect中添加
socket.on('receive-ice-candidate', (candidate) => {
  if (peer) {
    peer.addIceCandidate(new RTCIceCandidate(candidate));
  }
});

4. 发起方addTrack时机错误

发起方先创建Offer再调用addTrack,导致Offer的SDP不包含媒体轨道描述,对方不知道要接收媒体,不会触发ontrack。

修复:发起方先添加轨道,再创建Offer:

// opencamera函数内调整顺序
stream.getTracks().forEach(track => {
  newPeer.addTrack(track, stream);
});
const offer = await newPeer.createOffer();
await newPeer.setLocalDescription(new RTCSessionDescription(offer));
socket.emit('sending-offer', offer);

修复后的完整代码示例

import { useState, useEffect } from "react";
import io from 'socket.io-client'
import ReactPlayer from "react-player";

function App() {
  const [peer, setPeer] = useState(null);
  const socket = io('http://localhost:4080/')
  const [email, setEmail] = useState("");
  const [room, setRoom] = useState("");
  const [recivingcall, setrecivingcall] = useState(false)
  const [makingcall, setmakingcall] = useState(false)
  const [mystream, setmystream] = useState('')
  const [connected, setconnected] = useState(false)
  const [remotestream, setremotestream] = useState('')

  const makecall = () => {
    console.log("email ", email, " room ", room)
    socket.emit('join-room', { email, room })
  }

  const joincall = () => { }

  const opencamera = async () => {
    try {
      const stream = await navigator.mediaDevices.getUserMedia({ audio: false, video: true });
      setmystream(stream);
      const configuration = { 'iceServers': [{ 'urls': 'stun:stun.l.google.com:19302' }] }
      const newPeer = new RTCPeerConnection(configuration);

      // 绑定track事件
      newPeer.addEventListener('track', (event) => {
        const [remoteStream] = event.streams;
        setremotestream(remoteStream);
      });

      // 监听ICE候选并发送
      newPeer.addEventListener('icecandidate', (event) => {
        if (event.candidate) {
          socket.emit('send-ice-candidate', event.candidate);
        }
      });

      setPeer(newPeer);

      // 先添加轨道再创建Offer
      stream.getTracks().forEach(track => {
        newPeer.addTrack(track, stream);
      });

      const offer = await newPeer.createOffer();
      await newPeer.setLocalDescription(new RTCSessionDescription(offer));
      socket.emit('sending-offer', offer);
    } catch (err) {
      console.error('获取媒体流失败:', err);
    }
  }

  const creatanswer = async (offer) => {
    if (!offer) return;
    try {
      const configuration = { 'iceServers': [{ 'urls': 'stun:stun.l.google.com:19302' }] }
      const newPeer = new RTCPeerConnection(configuration);

      // 绑定track事件
      newPeer.addEventListener('track', (event) => {
        const [remoteStream] = event.streams;
        setremotestream(remoteStream);
      });

      // 监听ICE候选并发送
      newPeer.addEventListener('icecandidate', (event) => {
        if (event.candidate) {
          socket.emit('send-ice-candidate', event.candidate);
        }
      });

      setPeer(newPeer);

      await newPeer.setRemoteDescription(new RTCSessionDescription(offer));
      const ans = await newPeer.createAnswer();
      await newPeer.setLocalDescription(new RTCSessionDescription(ans));
      socket.emit('sending-answer', ans);

      const stream = await navigator.mediaDevices.getUserMedia({ audio: false, video: true });
      setmystream(stream);
      stream.getTracks().forEach(track => {
        newPeer.addTrack(track, stream);
      });
    } catch (err) {
      console.error('创建应答失败:', err);
    }
  }

  const hendelanswer_recived = async (ans) => {
    if (!ans) return;
    console.log(ans)
    const remotedes = new RTCSessionDescription(ans)
    await peer.setRemoteDescription(remotedes)
    console.log('peer', peer)
    socket.emit('connection-complete', 'ok')
  }

  useEffect(() => {
    socket.on('room-created', (ev) => {
      setconnected(true)
      opencamera()
    })
    socket.on('offer-recived', (offer) => {
      console.log(offer)
      creatanswer(offer)
    })
    socket.on('answer-recived', hendelanswer_recived)
    // 接收对方的ICE候选
    socket.on('receive-ice-candidate', (candidate) => {
      if (peer) {
        peer.addIceCandidate(new RTCIceCandidate(candidate));
      }
    })
    socket.on('connection-complete', () => {
      console.log('连接完成');
    })

    // 组件卸载时清理资源
    return () => {
      socket.disconnect();
      if (peer) {
        peer.close();
      }
      if (mystream) {
        mystream.getTracks().forEach(track => track.stop());
      }
    }
  }, [peer, mystream])

  return (
    <>
      <input id="email" onChange={(e) => setEmail(e.target.value)}></input>
      <input id="room" onChange={(e) => setRoom(e.target.value)}></input>
      {connected ? <h1>Connecting...</h1> : <> </>}
      {mystream && (
        <ReactPlayer
          playing
          muted
          height="100px"
          width="200px"
          url={mystream}
        />
      )}
      <button onClick={makecall}>makecall</button>
      {remotestream && (
        <>
          <h1>Remote Stream</h1>
          <ReactPlayer
            playing
            muted
            height="100px"
            width="200px"
            url={remotestream}
          />
        </>
      )}
      {recivingcall ? <button id="joincall" onClick={joincall}>joincall</button> : <> </>}
    </>
  )
}

export default App;

额外注意事项

  • 确保Socket.io服务器端实现ICE候选转发逻辑,收到send-ice-candidate后转发给房间内其他用户。
  • 增加错误捕获逻辑,避免媒体流获取、PeerConnection操作等环节静默失败。
  • 组件卸载时关闭PeerConnection并停止媒体轨道,防止资源泄漏。

内容的提问来源于stack exchange,提问作者mm m

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 03:45:00