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

介绍

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服务器是用于对等连接异常时的兜底方案,可进行数据传输。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【云原生】Prometheus监控Docker指标并接入Grafana

目录 一、前言 二、docker监控概述 2.1 docker常用监控指标 2.2 docker常用监控工具 三、CAdvisor概述 3.1 CAdvisor是什么 3.2 CAdvisor功能特点 3.3 CAdvisor使用场景 四、CAdvisor对接Prometheus与Grafana 4.1 环境准备 4.2 docker部署CAdvisor 4.2.2 docker部署…

汉诺塔与青蛙跳台阶

1.汉诺塔 根据汉诺塔 - 维基百科 介绍 1.1 背景 最早发明这个问题的人是法国数学家爱德华卢卡斯。 传说越南河内某间寺院有三根银棒&#xff0c;上串 64 个金盘。寺院里的僧侣依照一个古老的预言&#xff0c;以上述规则移动这些盘子&#xff1b;预言说当这些盘子移动完毕&am…

Java项目:基于SSM框架实现的共享客栈管理系统分前后台【ssm+B/S架构+源码+数据库+毕业论文】

一、项目简介 本项目是一套基于SSM框架实现的共享客栈管理系统 包含&#xff1a;项目源码、数据库脚本等&#xff0c;该项目附带全部源码可作为毕设使用。 项目都经过严格调试&#xff0c;eclipse或者idea 确保可以运行&#xff01; 该系统功能完善、界面美观、操作简单、功能…

网页生成二维码、在线演示

https://andi.cn/page/621504.html

go语言day11 错误 defer(),panic(),recover()

错误&#xff1a; 创建错误 1&#xff09;fmt包下提供的方法 fmt.Errorf(" 格式化字符串信息 " &#xff0c; 空接口类型对象 ) 2&#xff09;errors包下提供的方法 errors.New(" 字符串信息 ") 创建自定义错误 需要实现error接口&#xff0c;而error接口…

【JAVA多线程】线程池概论

目录 1.概述 2.ThreadPoolExector 2.1.参数 2.2.新任务提交流程 2.3.拒绝策略 2.4.代码示例 1.概述 线程池的核心&#xff1a; 线程池的实现原理是个标准的生产消费者模型&#xff0c;调用方不停向线程池中写数据&#xff0c;线程池中的线程组不停从队列中取任务。 实现…

“未来已来·智能共融”高峰论坛在京成功举办

在人工智能技术的澎湃浪潮中,其与传统产业的深度融合正逐步成为驱动区域经济增长的新引擎。2024年7月4号,一场以“未来已来智能共融——探索人类智能与人工智能共生共进的新路径”为主题的高峰论坛在北京电子科技职业学院图书馆圆满落幕,为北京经济技术开发区(简称“北京经开区…

时间处理的未来:Java 8全新日期与时间API完全解析

文章目录 一、改进背景二、本地日期时间三、时区日期时间四、格式化 一、改进背景 Java 8针对时间处理进行了全面的改进&#xff0c;重新设计了所有日期时间、日历及时区相关的 API。并把它们都统一放置在 java.time 包和子包下。 Java5的不足之处&#xff1a; 非线程安全&…

JAVA 课设 满汉楼餐厅点餐系统

一、代码详解 1.总体结构展示 2.总体代码 2.1 libs文件 链接&#xff1a;https://pan.baidu.com/s/1nH-I7gIlsqyMpXDDCFRuOA 提取码&#xff1a;3404 2.2 配置的德鲁连接池 #keyvalue driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/mhl?rewriteBa…

SAP_MM模块-特殊业务场景下的系统实现方案

一、业务背景 目前公司有一种电商业务&#xff0c;卖的是备品配件&#xff0c;是公司先跟供应商采购&#xff0c;然后再销售给客户&#xff0c;系统账就是按照正常业务来流转&#xff0c;公司进行采购订单入库&#xff0c;然后销售订单出库。 不过这种备品配件&#xff0c;实…

Android使用http加载自建服务器静态网页

最终效果如下图&#xff0c;成功加载了电脑端的静态网页内容&#xff0c;这是一个xml文件。 电脑端搭建http服务器 使用“Apache Http Server”&#xff0c;下载地址是&#xff1a;https://httpd.apache.org/download.cgi。具体操作步骤&#xff0c;参考&#xff1a;Apache …

卫星IoT产品发展前景

卫星IoT产品发展前景 一、概述 卫星IoT产品是指利用卫星通信技术实现物联网设备互联互通的解决方案。随着卫星互联网技术的快速发展&#xff0c;卫星IoT产品正逐渐成为解决偏远地区、海洋、航空等场景下物联网连接问题的重要手段。 二、性能特点 广泛覆盖&#xff1a; 卫星…

ssrf结合redis未授权getshell

目录 漏洞介绍 SSRF Redis未授权 利用原理 环境搭建 利用过程 rockylinux cron计划任务反弹shell 写公钥免密登录 ubuntu 写公钥免密登录 漏洞介绍 SSRF SSRF&#xff08;server side request forgrey&#xff09;服务端请求伪造&#xff0c;因后端未过滤用户输入&…

SpringBoot实现多数据源切换

1. 概述 仓库地址&#xff1a;https://gitee.com/aopmin/multi-datasource-demo 随着项目规模的扩大和业务需求的复杂化&#xff0c;单一数据源已经不能满足实际开发中的需求。在许多情况下&#xff0c;我们需要同时操作多个数据库&#xff0c;或者需要将不同类型的数据存储在不…

陶建辉当选 GDOS 全球数据库及开源峰会荣誉顾问

近日&#xff0c;第二十三届 GOPS 全球运维大会暨 XOps 技术创新峰会在北京正式召开。本次会议重点议题方向包括开源数据库落地思考、金融数据库自主可控、云原生时代下数据库、数据库智能运维、数据库安全与隐私、开源数据库与治理。大会深入探讨这些方向&#xff0c;促进了数…

Matplotlib 学习

知识点 1.plot()&#xff1a;用于绘制线图和 散点图scatter() 函数&#xff1a;plot() 函数可以接受许多可选参数&#xff0c;用于控制图形的外观&#xff0c;例如&#xff1a;颜色: colorblue 控制线条的颜色。线型: linestyle-- 控制线条的样式&#xff0c;例如虚线。标记…

前端vue后端java使用easyexcel框架下载表格xls数据工具类

一 使用alibaba开源的 easyexcel框架&#xff0c;后台只需一个工具类即可实现下载 后端下载实现 依赖 pom.xml <dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>4.1.2</version></dependen…

昇思25天学习打卡营第12天|FCN图像语义分割

文章目录 昇思MindSpore应用实践基于MindSpore的FCN图像语义分割1、FCN 图像分割简介2、构建 FCN 模型3、数据预处理4、模型训练自定义评价指标 Metrics 5、模型推理结果 Reference 昇思MindSpore应用实践 本系列文章主要用于记录昇思25天学习打卡营的学习心得。 基于MindSpo…

机械键盘有哪些分类

机械键盘是一种比传统的薄膜键盘更耐用、更快捷、更具有手感的键盘。它的键帽和按键是独立的&#xff0c;能够提供更好的反应速度和操作感。机械键盘在现代化生活中得到了广泛的应用。根据其特性和使用场景&#xff0c;机械键盘可以分为以下几类&#xff1a; 1.轴体分类 机械…

永磁同步电机控制算法--最大转矩电流比控制(虚拟信号注入法)

目前&#xff0c;国内外相关学者对 MTPA 控制方法进行了一系列的理论研究与仿真分析。通过研究取得的成果综合来看&#xff0c;该控制方法主要有&#xff1a;直接公式计算法、曲线拟合法、查表法、搜索法、高频信号注入法以及参数辨识法等。 之前的文章中已经介绍了直接公式计…