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

设置video.srcObject后WebRTC无法播放远端对等方流的问题

家庭监控系统WebRTC流播放问题

项目背景

正在开发一个简易家庭监控系统,支持以推流端或消费端身份进入应用。

技术栈

  • socket.io - 用于推流端与对等方之间的信令传输
  • 基础Vite应用(React)
  • WebRTC - 用于在两个对等方之间创建流通道

开发仅使用localhost,客户端和服务器均在Windows的WSL终端运行,通过Windows机器访问应用。

当前表现

访问http://localhost/?stream后启动推流,等待WebSocket消息以与新对等方启动信令交互。
当消费端访问http://localhost时,WebRTC连接建立,接收来自推流端的轨道。
消费端的peerConnection.ontrack事件已触发,但媒体流似乎仅包含1条静音的视频轨道。
推流端调用peerConnection.addTrack时添加了2条轨道,分别为视频和音频轨道。

问题

消费端接收推流端的轨道/流并设置到video.srcObject属性后,无任何反应,视频和音频均无法播放。

预期表现

消费端设置video.srcObject后,应能播放推流端的视频和音频。

已尝试方案

查阅相关问题并尝试了多种建议方案,但均未解决问题:

  • 将video元素的autoPlay、playsInline和muted设为true
  • 设置远端video.srcObject后通过代码调用视频播放
  • 使用STUN服务器 - 但认为本地环境无需该服务
  • 消费端点击按钮后异步连接对等方 - 避免无用户交互时播放视频

核心代码

信令WebSocket服务

import { Server } from "socket.io";

const io = new Server();

let streamer = null;
let users = new Set();

io.on("connect", (socket) => {
  if (users.has(socket.id)) {
    return;
  }

  console.log(
    `Socket ${socket.id} connected - Client IP Address: ${socket.handshake.address}`
  );

  socket.on("disconnect", (reason) => {
    console.log(`Socket: ${socket.id} disconnected - Reason: ${reason}`);
  });

  socket.on("begin-stream", () => {
    console.log("begin stream", socket.id);
    streamer = socket.id;
  });

  socket.on("request-start-stream", (data) => {
    console.log("request-start-stream");
    socket.to(streamer).emit("handle-request-start-stream", {
      to: socket.id,
      offer: data.offer,
    });
  });

  socket.on("response-start-stream", (data) => {
    console.log("response-start-stream");
    socket.to(data.to).emit("handle-response-start-stream", data.answer);
  });
});

io.listen(5432, { cors: true });

Streamer.tsx

import { useEffect } from "react";

import socket from "./socket";
import useVideo from "./useVideo";

export default function Streamer() {
  const { videoRef, element: videoElement } = useVideo();

  useEffect(() => {
    init();

    async function init() {
      if (!videoRef.current) return;

      const stream = await navigator.mediaDevices.getUserMedia({
        video: true,
        audio: true,
      });

      videoRef.current.srcObject = stream;

      socket.emit("begin-stream");

      socket.on("handle-request-start-stream", async ({ to, offer }) => {
        const peerConnection = new RTCPeerConnection();
        stream
          .getTracks()
          .forEach(
            (track) =>
              console.log("track", track) ||
              peerConnection.addTrack(track, stream)
          );

        await peerConnection.setRemoteDescription(
          new RTCSessionDescription(offer)
        );

        const answer = await peerConnection.createAnswer();
        await peerConnection.setLocalDescription(
          new RTCSessionDescription(answer)
        );
        socket.emit("response-start-stream", { to, answer });
      });
    }
  }, []);

  return videoElement;
}

Consumer.tsx

import { useEffect } from "react";

import socket from "./socket";
import useVideo from "./useVideo";

export default function Consumer() {
  const { videoRef, element: videoElement } = useVideo();

  useEffect(() => {
    init();

    async function init() {
      const peerConnection = new RTCPeerConnection();
      peerConnection.addEventListener("track", ({ streams: [stream] }) => {
        if (!videoRef.current) return;
        console.log('stream', stream)

        videoRef.current.srcObject = stream;
      });

      const offer = await peerConnection.createOffer({
        // workaround to receive the tracks
        // if this is not specified, I will never receive the tracks
        offerToReceiveAudio: true,
        offerToReceiveVideo: true,
      });
      await peerConnection.setLocalDescription(
        new RTCSessionDescription(offer)
      );
      socket.emit("request-start-stream", { offer });

      socket.on("handle-response-start-stream", async (answer) => {
        await peerConnection.setRemoteDescription(
          new RTCSessionDescription(answer)
        );
      });
    }
  }, []);

  return videoElement;
}

useVideo.tsx(视频标签Hook)

import { useRef } from "react";

export default function useVideo() {
  const videoRef = useRef<HTMLVideoElement>(null);

  const element = <video ref={videoRef} autoPlay />;

  return { videoRef, element };
}

内容的提问来源于stack exchange,提问作者Jesús González

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 19:34:51