【WebRTC实现点对点视频通话】

news2025/1/15 6:59:16

介绍

WebRTC (Web Real-Time Communications) 是一个实时通讯技术,也是实时音视频技术的标准和框架。简单来说WebRTC是一个集大成的实时音视频技术集,包含了各种客户端api、音视频编/解码lib、流媒体传输协议、回声消除、安全传输等。对于开发者来说可以借助webrtc非常方便的实现低延时视频通话能力。目前大多主流的直播系统、会议系统基本都是基于WebRTC来实现。

三种架构

WebRTC针对不同场景以及性能考虑提供了三种架构Mesh架构、MCU、FSU。
在这里插入图片描述

Mesh架构

Mesh架构,需要所有参与连接的peer建立与所有其他peer的媒体连接(两两连接)。该架构需要n-1个上下行,以此带来的带宽消耗(流量)、编/解码消耗(设备性能)成线性增长。该架构只能适用3-4个人的小型会议场景。

MCU架构

所有参与连接的peer将本地媒体流推到远程媒体服务器,由媒体服务器进行混流,然后再推到所有连接的peer端。该架构的优点就是只需要1路上下行,随着peer人数不断增加,依然不会对用户造成带宽、手机性能影响。该架构将压力转嫁到服务端,由专用媒体服务器来完成混流,转推等功能。

SFU架构

相对于MCU来说SFU只做转发,媒体服务器压力有限。与Mesh架构相比,只需要n-1个下行,1个上行,减少了服务器压力。在大规模的场合该架构具有伸缩性。

点对点视频连接

根据上面,我们对基本WebRTC有了最基本的认识,下面就从点对点实际例子来从代码角度进一步了解其原理。

先从下图来看看使用MCU来实现点对点需要哪些东西在这里插入图片描述
在介绍流程之前,先简单介绍下上图中出现的名词代表什么意思:

  • Peer:通信双方设备。
  • Signaling Server: 信令服务器,用于交互连接双方的信令数据(SDP、ICE等),以保证通信的对等连接建立。
  • NAT:处理私有网络和公共网络之间的地址转换问题(因为大多数设置都处于内网中,需要转换为公共网络才能进行外网访问)
  • STUN:用于发现设备的公共地址(通过NAT转换的公网地址),辅助穿越NAT进行点对点连接。
  • TURN:在无法建立直接连接时提供数据中继,确保通信的可靠性。对等连接异常时的兜底方案。
  • SDP:会话描述协议,用于描述和协商媒体会话的协议,它定义了会话的所有技术细节,包括媒体格式、编解码器、网络地址等。,
  • ICE:用于发现和选择最优网络路径的框架,确保在各种网络环境下都能成功建立和维持连接。

代码实现

实现点对点连接主要是两点:1、信令数据交互 2、对等连接建立
在代码中使用到了socket.io来将设备和信令服务器通信,使用了simple-peer来建立对等连接,由于该demo在本地运行所以没有使用STUN/TRUN服务器,有兴趣的可以使用Chrome提供的公共服务器stun:stun.l.google.com:19302
主要步骤如下:

  • 1、和信令服务器建立连接,并获取自身的socketId作为唯一标识
  • 2、申请方将信令(由simple-peer生成)通过信令服务器到达接受方
  • 3、接受方接受,将发起方的信令保存到对等连接peer中,并且将自己的信令通过信令服务器给到发送方
  • 4、发送方将接受方的信令数据保存到对等连接peer中,至此发送方-接受方对等连接建立完成
  • 5、在发送方和接受方监听peer的stream,来获取视频流,然后展示在页面

和信令服务器建立连接

新建一个server,js使用node+express搭建的简易信令服务器,用于交换双方信令。通过create-react-app来创建一个前端页面。

信令服务器代码如下:

const express = require("express");
const http = require("http");
const cors = require("cors");

const app = express();
const server = http.createServer(app);
app.use(cors);
const io = require("socket.io")(server, {
  cors: {
    origin: "*",
    methods: ["POST", "GET"],
  },
});

server.listen(5001, () => {
  console.log("listening on 5000 ...");
});

io.on("connection", (socket) => {
  // 分发socket id
  socket.emit("offer", socket.id);

  // 发送发起方的信令数据别answer
  socket.on("callUser", (data) => {
    io.to(data.answerId).emit("callUser", data);
  });

  // 发送接收放信令给申请方
  socket.on("answerSignalInfo", (data) => {
    io.to(data.to).emit("answerSignalInfo", data);
  });

  socket.on("disconnect", () => {
    socket.broadcast.emit("callEnded", socket);
  });
});
// frontend
// 通过socket.io和服务器进行连接
const socket = io("http://localhost:5001");

// 获取自身的socket id
socket.on("offer", (offerId) => {
  console.log("offer socket ID", offerId);
  setOfferId(offerId);
  getLocalStream(); // 获取本地视频流
});

传递信令数据

// 通过simple-peer 交换信令数据 offer -> 信令服务器 -> answer
const peer = new Peer({
  initiator: true, // 是否是发起方
  stream: localStream, // 传递的视频流
  trickle: false, // 点对点传输,获取单个信号
  // 设置STUN服务器,Chrome提供的公共服务器
  config: {
    iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
  },
});
peer.on("signal", (data: any) => {
  socket.emit("callUser", {
    singleData: data, // 发送通话方的信令数据
    answerId: answerId, // 需要和谁通话
    from: offerId, // 谁申请通话
  });
});

接收信令数据

接收方接收发起方的信令数据,并保存到Peer中,然后将自身的信令数据返回给发起方

const peer = new Peer({
  initiator: false,
  stream: localStream,
  trickle: false,
  config: {
    iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
  },
});
peer.on("signal", (data) => {
  socket.emit("answerSignalInfo", {
    answerSignalInfo: data,
    to: offerUserInfo?.id,
    from: offerId,
  });
});

if (offerUserInfo?.singleData) {
  peer.signal(offerUserInfo.singleData);
}

对等连接建立,获取双方视频流

交互信令之后,通过simple-peer成功建立对等连接,监听stream视频流然后显示在页面上

// 监听通过对等连接传递的stream
peer.on("stream", (stream) => {
  if (remoteVideoRef.current) {
    remoteVideoRef.current.srcObject = stream;
    remoteVideoRef.current.play();
  }
});

完整页面代码:CSS样式文件省略

import React, { useCallback, useEffect, useRef, useState } from "react";
import { io } from "socket.io-client";
import Peer from "simple-peer";
import "./App.css";

const socket = io("http://localhost:5001");

type UserInfo = {
  singleData: any;
  id: string;
};

function App() {
  // 用于引用 DOM 元素
  const localVideoRef = useRef<HTMLVideoElement>(null);
  const remoteVideoRef = useRef<HTMLVideoElement>(null);

  // 用于管理状态
  const [localStream, setLocalStream] = useState<MediaStream | undefined>();
  const [offerId, setOfferId] = useState("");
  const [answerId, setAnswerId] = useState("");
  const [offerUserInfo, setOfferUserInfo] = useState<UserInfo>();

  // 获取本地视频流
  const getLocalStream = useCallback(async () => {
    try {
      const stream = await navigator.mediaDevices.getUserMedia({
        video: {
          width: { ideal: 200 }, // 理想的宽度
          height: { ideal: 200 }, // 理想的高度
        },
        audio: false,
      });
      console.log("local media", stream);
      setLocalStream(stream);
      if (localVideoRef.current) {
        localVideoRef.current.srcObject = stream;
      }
    } catch (error) {
      console.error("Error accessing media devices.", error);
    }
  }, []);

  // 手动设置通话方id
  const onChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
    console.log("onChange call id", e);
    setAnswerId(e.target.value);
  }, []);

  // 获取信令牌服务器发送的socket id
  const init = useCallback(() => {
    socket.on("offer", (offerId) => {
      console.log("offer socket ID", offerId);
      setOfferId(offerId);
      getLocalStream(); // 获取本地视频流
    });

    // 监听信令服务器发送的通话申请方的信令牌数据
    socket.on("callUser", ({ singleData, answerId, from }) => {
      console.log(`${from}发起通话`, from);
      setOfferUserInfo({
        singleData: singleData,
        id: from,
      });
    });
  }, [getLocalStream]);

  // 创建和发送 offer
  const startCall = useCallback(async () => {
    // 通过simple-peer 交换信令数据 offer -> 信令服务器 -> answer
    const peer = new Peer({
      initiator: true,
      stream: localStream,
      trickle: false,
      // 设置STUN服务器,Chrome提供的公共服务器
      config: {
        iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
      },
    });
    peer.on("signal", (data: any) => {
      socket.emit("callUser", {
        singleData: data, // 发送通话方的信令数据
        answerId: answerId, // 需要和谁通话
        from: offerId, // 谁申请通话
      });
    });

    // 获取到接收方的信令数据
    socket.on("answerSignalInfo", (data) => {
      console.log(`${data.from}已经接受通话`, data, peer);
      peer.signal(data.answerSignalInfo);
    });

    // 监听通过对等连接传递的stream
    peer.on("stream", (stream) => {
      if (remoteVideoRef.current) {
        remoteVideoRef.current.srcObject = stream;
        remoteVideoRef.current.play();
      }
    });
    // setPeer(peer);
  }, [answerId, localStream, offerId, remoteVideoRef]);

  const acceptCall = useCallback(() => {
    const peer = new Peer({
      initiator: false,
      stream: localStream,
      trickle: false,
      config: {
        iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
      },
    });
    peer.on("signal", (data) => {
      socket.emit("answerSignalInfo", {
        answerSignalInfo: data,
        to: offerUserInfo?.id,
        from: offerId,
      });
    });

    if (offerUserInfo?.singleData) {
      peer.signal(offerUserInfo.singleData);
    }

    // 监听通过对等连接传递的stream
    peer.on("stream", (stream) => {
      if (remoteVideoRef.current) {
        remoteVideoRef.current.srcObject = stream;
        remoteVideoRef.current.play();
      }
    });
  }, [localStream, offerUserInfo, offerId, remoteVideoRef]);

  useEffect(() => {
    init();
  }, [init]);

  return (
    <div className="App">
      <video autoPlay muted ref={localVideoRef} className="video" />
      <video autoPlay muted ref={remoteVideoRef} className="video" />
      <input value={answerId} onChange={onChange} placeholder="call id" />
      <button onClick={startCall}>发起通话</button>
      <button onClick={acceptCall}>同意通话</button>
    </div>
  );
}

export default App;

至此可以启动项目,并本地浏览器打开两个tab即可体验点对点视频服务。

总结

点对点通信,主要就是信令数据的交换,知道通信双方具体的配置信息(通信参数、IP地址等)以保证对等连接的成功建立,然后传递视频流在页面展示。
其中信令服务器仅用于对等连接前的信令交换,不会进行数据传输。NAT是将设备内网地址转换为外网公共地址。STUN来获取设置的公网地址。TURN服务器是用于对等连接异常时的兜底方案,可进行数据传输。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1895590.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【matlab】智能优化算法——求解目标函数

智能优化算法在求解目标函数方面发挥着重要作用&#xff0c;它通过迭代、筛选等方法来寻找目标函数的最优值&#xff08;极值&#xff09;。以下是关于智能优化算法求解目标函数的详细介绍&#xff1a; 一、智能优化算法概述 智能优化算法是一种搜索算法&#xff0c;旨在通过…

吴恩达机器学习 第三课 week2 推荐算法(下)

目录 01 学习目标 02 基于内容的过滤算法 03 实现“电影推荐系统” 3.1 问题描述 3.2 算法实现 04 大项目&#xff08;数据很大&#xff09;的推荐方法※ 4.1 方法原理 4.2 实施示例 05 总结 01 学习目标 &#xff08;1&#xff09;理解基于内容的过滤算法&#xff08…

模拟退火算法4—应用

TSP&#xff08;旅行商&#xff09;问题是最有代表性的优化组合问题之一&#xff0c;其应用已逐步渗透到各个技术领域和我们的日常生活中.它一开始是为交通运输而提出的&#xff0c;比如飞机航线安排、送邮件、快递服务、设计校车行进路线等等.实际上其应用范围扩展到了许多其他…

阿里模型调用体验

引言 随着人工智能技术的飞速发展&#xff0c;大型模型已成为推动技术进步的关键因素之一。阿里大模型作为国内领先的人工智能技术之一&#xff0c;其在多个领域的应用展示了强大的潜力。本文将通过调用案例&#xff0c;简单解析阿里大模型在特定场景中的应用及其效果。 1.导…

Aigtek高压功率放大器主要应用场景是什么

高压功率放大器是一种关键的电子设备&#xff0c;其主要功能是将低电压信号放大到较高电压水平&#xff0c;以满足特定应用的需求。这种类型的放大器在各种领域都发挥着至关重要的作用。安泰电子官网将为大家介绍高压功率放大器的主要应用场景&#xff0c;并介绍其在这些领域中…

Qt 网络编程实战

一.获取主机的网络信息 需要添加network模块 QT core gui network主要涉及的类分析 QHostInfo类 QHostInfo::localHostName() 获取本地的主机名QHostInfo::fromName(const QString &) 获取指定主机的主机信息 addresses接口 QNetworkInterface类 QNetworkInterfac…

《操作系统真象还原》学习笔记:第2章——编写MBR主引导记录

2.1 计算机的启动过程 载入内存&#xff1a; &#xff08;1&#xff09; 程序被加载器&#xff08;软件或硬件&#xff09;加载到内存某个区域 &#xff08;2&#xff09;CPU 的 cs:ip 寄存器被指向这个程序的起始地址 2.2 软件接力第一棒&#xff0c;BIOS 2.2.1 实模式下的…

78110A雷达信号模拟软件

78110A雷达信号模拟软件 78110A雷达信号模拟软件(简称雷达信号模拟软件)主要用于模拟产生雷达发射信号和目标回波信号&#xff0c;软件将编译生成的雷达信号任意波数据下载到信号发生器中&#xff0c;主要是1466-V矢量信号发生器&#xff0c;可实现雷达信号模拟产生。软件可模…

结构方程模型-验证性因子分析模型

初级 第7讲 验证性因子分析模_哔哩哔哩_bilibili

【matlab】【python】爬虫实战

目录 引言 具体步骤 1.设置请求选项 2.发送请求并获取响应 3.设置正则表达式 4.执行正则表达式匹配 matlab完整代码 python代码示例 引言 在当今这个信息爆炸的时代&#xff0c;数据已成为推动社会进步和企业发展的核心动力之一。随着互联网的普及和技术的飞速发展&am…

7.Android逆向协议-抓取安卓http和https数据包(设备需要root权限)

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 内容参考于&#xff1a;微尘网校 上一个内容&#xff1a;6.Android逆向协议-配置FD抓包环境 root&#xff1a; 现在的安卓手机不好搞&#xff0c;有很多坑&am…

ECSNet: Spatio-Temporal Feature Learning for Event Camera

标题&#xff1a;ECSNet&#xff1a;事件相机的空间时间特征学习 源文&#xff1a;https://ieeexplore.ieee.org/stamp/stamp.jsp?tp&arnumber9869656https://ieeexplore.ieee.org/stamp/stamp.jsp?tp&arnumber9869656 源码&#xff1a;GitHub - zhiwen-xdu/ECSNet…

AIGC为设计师解决了什么问题?

在当今数字化时代&#xff0c;设计师们面临着前所未有的挑战和压力。他们不仅要不断创新以满足市场和客户的需求&#xff0c;还要紧跟快速变化的设计趋势和技术发展的步伐。幸运的是&#xff0c;随着生成式人工智能&#xff08;AIGC&#xff09;的兴起&#xff0c;设计师们找到…

Prometheus安装部署

1 常见部署方式 包安装 RHEL系统: https://packagecloud.io/app/prometheus-rpm/release/search 二进制安装 https://prometheus.io/download/ 基于 Docker 运行 https://prometheus.io/docs/prometheus/latest/installation/ 1.1 Docker 镜像直接启动 [root120 ~]# d…

日志自动提取---七牛Logkit观星应急工具

目录 七牛Logkit (Windows&Linux&Mac 等) 下载: 文档: windows配置过程: 1-下载 2-修改logkit-community基本配置 3-启动! 4-浏览器访问 5-添加配置吧 观星应急工具 &#xff08;Windows 系统日志&#xff09; 七牛Logkit (Windows&Linux&Mac 等) -…

深度学习图像生成与分割模型详解:从StyleGAN到PSPNet

文章目录 Style GANDeeplab-v3FCNAdversarial AutoencodersHigh-Resolution Image Synthesis with Latent Diffusion ModelsNeRF: Representing Scenes as Neural Radiance Fields for View SynthesisPyramid Scene Parsing Network Style GAN 输入是一个潜在向量 (z)&#xff…

SPI四种模式--极性与相位

SPI的四种模式&#xff1a;相位和极性 极性 定义时钟空闲状态&#xff1a; CPOL0&#xff1a;时钟线在空闲状态为低电平 CPOL1&#xff1a;时钟线在空闲状态为高电平 这个设置决定了设备不进行通信时时钟线的状态。 兼容性&#xff1a; 不同的SPI设备可能需要不同的时钟极性…

Elasticsearch:Ingest architectures - 摄取架构

我们提供各种采集架构&#xff0c;以满足各种用例和网络配置的需求。 要将数据采集到 Elasticsearch&#xff0c;请使用最符合你的需求和用例的选项。对于许多用户和用例来说&#xff0c;最简单的方法是使用 Elastic Agent 采集数据并将其发送到 Elasticsearch。Elastic Agent…

Rhino 犀牛三维建模工具下载安装,Rhino 适用于机械设计广泛领域

Rhinoceros&#xff0c;这款软件小巧而强大&#xff0c;无论是机械设计、科学工业还是三维动画等多元化领域&#xff0c;它都能展现出其惊人的建模能力。 Rhinoceros所包含的NURBS建模功能&#xff0c;堪称业界翘楚。NURBS&#xff0c;即非均匀有理B样条&#xff0c;是计算机图…

PTrade量化软件常见问题整理系列2

一、研究界面使用get_fundamentals函数报错&#xff1a;error_info:获取token失败&#xff1f; 研究界面使用get_fundamentals函数报错&#xff1a;error_info:获取token失败&#xff1f; 1、测试版本202202.01.052&#xff0c;升级202202.01.051版本后&#xff0c;为了解决不…