【WebRTC】音视频通信

WebRTC对等体还需要查找并交换本地和远程音频和视频媒体信息,例如分辨率和编解码器功能。 交换媒体配置信息的信令通过使用被称为SDP的会话描述协议格式来交换,被称为提议和应答的元数据块

WebRTC 音视频通信基本流程

  • 一方发起调用 getUserMedia 打开本地摄像头
  • 媒体协商(信令交换,媒体协商主要指 SDP 交换。)
  • 建立通信

发起端 Amy 创建 Offer 并将 Offer 信息,并调用 setLocalDescription 将其保存起来,通过信令服务器传送给接收端 Bob
接收端 Bob 收到对等端 Amy 的 Offer 信息后调用 setRemoteDescription 方法将其保存起来,并创建 Answer 信息,同理也将 Answer 消息通过 setLocalDescription 保存,并通过信令服务器传送给呼叫端 Amy
呼叫端 Amy 收到对等端 Blob 的 Answer 信息后调用 setRemoteDescription 方法将其 Answer 保存起来

为什么需要媒体协商?

媒体协商的作用就是让双方找到共同支持的媒体能力,从而能实现彼此之间的音视频通信。比如两个人想聊天,一个人只会讲中文,喜欢讨论前端;一个人只会讲英文,不喜欢前端技术;通过互换资料,发现这没法聊天。但如果出现另一个人会讲中文,喜欢研究代码和前端技术,跟第一个人交换信息后,发现这可以聊天。所以发起端与接收端能不能进行通信。

媒体协商是在做什么?

媒体协商就是在交换 SDP的过程。会话发起者通过创建一个offer,经过信令服务器发送到接收方,接收方创建answer并返回给发送方,完成交换。

SDP 是什么?

SDP(Session Description Protocol)指会话描述协议,是一种通用的协议,基于文本,其本身并不属于传输协议,需要依赖其它的传输协议(如 RTP 交换媒体信息。

SDP 主要用来描述多媒体会话,用途包括会话声明、会话邀请、会话初始化等。通俗来讲,它可以表示各端的能力,记录有关于你音频编解码类型、编解码器相关的参数、传输协议等信息。

交换 SDP 时,通信的双方会将接受到的 SDP 和自己的 SDP 进行比较,取出他们之间的交集,这个交集就是协商的结果,也就是最终双方音视频通信时使用的音视频参数及传输协议。

offer 和 answer 是什么?

在双方要建立点对点通信时,发起端发送的 SDP 消息称为 Offer,接收端发送的 SDP 消息称为 Answer
所以,offer 和 answer 本质就是存有 SDP 信息的对象,所以也会叫做 SDP Offer 和 SDP Answer。

信令与信令服务器

信令通常指的是为了网络中各种设备协调运作,在设备之间传递的控制信息。

对于 WebRTC 通信来说,发起端发送 Offer SDP 和接收端接受 Answer
SDP,要怎么发给对方呢?这个过程还需要一种机制来协调通信并发送控制消息,这个过程就称为信令。

而信令对应的服务器就叫信令服务器,作为中间人帮助建立连接,主要负责:
信令的处理,如媒体协商消息的传递

管理房间信息。比如用户连接时告诉信令服务器自身的房间号,由信令服务器找到也在该房间号的对等端并开始尝试通信,也通知用户谁加入了房间和离开了房间,通知房间人数是否已满等等,所以也叫信令服务器也叫房间服务器。

WebRTC 并没有规定信令必须使用何种实现,目前业界使用较多的是 WebSocket + JSON/SDP 的方案。其中 WebSocket 用来提供信令传输通道,JSON/SDP 用来封装信令的具体内容。

ICE

当媒体协商完成后,WebRTC 就开始建立网络连接,其过程称为 ICE(Interactive Connectivity Establishment)交互式连接建立。

ICE 不是一种协议,整合了 STUN 和 TURN 两种协议(用于 NAT 穿透)的框架。

ICE 是在各端调用 setLocalDescription() 后就开始了,其操作过程如下:

  • 收集 Candidate
  • 交换 Candidate
  • 按优先级尝试连接

什么是 Candidate?

比如想用 socket 连接某台服务器,一定要知道这台服务器的一些基本信息,如服务器的 IP 地址、端口号以及使用的传输协议。只有知道了这些信息,才能与这台服务器建立连接。而 Candidate 正是 WebRTC 用来描述它可以连接的远端的基本信息,因此 Candidate 是至少包括 IP 地址、端口号、协议的一个信息集。

const peerA = new RTCPeerConnection()
const peerB = new RTCPeerConnection()
API
pc.createOffer:创建 offer 方法,方法会返回 SDP Offer 信息
pc.setLocalDescription 设置本地 SDP 描述信息
pc.setRemoteDescription:设置远端的 SDP 描述信息,即对方发过来的 SDP 信息
pc.createAnswer:远端创建应答 Answer 方法,方法会返回 SDP Offer 信息
pc.ontrack:设置完远端 SDP 描述信息后会触发该方法,接收对方的媒体流
pc.onicecandidate:设置完本地 SDP 描述信息后会触发该方法,打开一个连接,开始运转媒体流
pc.addIceCandidate:连接添加对方的网络信息
pc.setLocalDescription:将 localDescription 设置为 offer,localDescription 即为我们需要发送给应答方的 sdp,此描述指定连接本地端的属性,包括媒体格式
pc.setRemoteDescription:改变与连接相关的描述,该描述主要是描述有些关于连接的属性,例如对端使用的解码器

RTCPeerConnection就代表着一个peer的连接,它是WebRTC应用核心。

  • 1、调用peerA (RTCPeerConnection对象) createOffer方法准备创建SDP
  • 2、在createOffer的回调方法里,同时做了这两件事

a、调用peerA的setLocalDescription(description)方法,这个方法会触发peerA的icecandidate
监听方法handleConnection. 在这个方法里,会将peerA的icecandidate发送给peerB.
然后PeerB执行addIceCandidate(candidate),将peerA的candidate登记在案.
b、将peerA的description (就是SDP)发送给peerB

  • 3、peerB收到peerA发来的SDP,执行createAnswer,在这个回调方法里,同时做两件事

a、调用peerB的setLocalDescription(description)方法,这个方法会触发peerB的icecandidate监听方法handleConnection,在这个方法里,会将peerB的icecandidate发送给peerA.
peerA收到后执行addIceCandidate(candidate),将peerB的candidate也登记
b、将peerB的SDP发送给peerA.

  • 4、peerA和peerB开始传递音视频

总结:整个过程就是peerA和peerB互相交换iceCandidate和SDP的过程。
在这里插入图片描述

import { useEffect, useRef, useState } from 'react'
import './App.css'

function App() {
  const localVideoRef = useRef<HTMLVideoElement>(null)
  const remoteVideoRef = useRef<HTMLVideoElement>(null)
  const pc = useRef<RTCPeerConnection>()
  const localStreamRef = useRef<MediaStream>()
  const wsRef = useRef(new WebSocket('ws://127.0.0.1:1234'))
  const username = (Math.random() + 1).toString(36).substring(7)
  const [status, setStatus] = useState('开始通话')

  useEffect(() => {
    initWs()
    getMediaDevices().then(() => {
      createRtcConnection()
      addLocalStreamToRtcConnection()
    })
  }, [])

  const initWs = () => {
    wsRef.current.onopen = () => console.log('ws 已经打开')
    wsRef.current.onmessage = wsOnMessage
  }

  const wsOnMessage = (e: MessageEvent) => {
    const wsData = JSON.parse(e.data)
    console.log('wsData', wsData)

    const wsUsername = wsData['username']
    console.log('wsUsername', wsUsername)
    if (username === wsUsername) {
      console.log('跳过处理本条消息')
      return
    }

    const wsType = wsData['type']
    console.log('wsType', wsType)

    if (wsType === 'offer') {
      const wsOffer = wsData['data']
      pc.current?.setRemoteDescription(new RTCSessionDescription(JSON.parse(wsOffer)))
      setStatus('请接听通话')
    }
    if (wsType === 'answer') {
      const wsAnswer = wsData['data']
      pc.current?.setRemoteDescription(new RTCSessionDescription(JSON.parse(wsAnswer)))
      setStatus('通话中')
    }
    if (wsType === 'candidate') {
      const wsCandidate = JSON.parse(wsData['data'])
      pc.current?.addIceCandidate(new RTCIceCandidate(wsCandidate))
      console.log('添加候选成功', wsCandidate)
    }
  }

  const wsSend = (type: string, data: any) => {
    wsRef.current.send(JSON.stringify({
      username,
      type,
      data,
    }))
  }

  const getMediaDevices = async () => {
    const stream = await navigator.mediaDevices.getUserMedia({
      video: true,
      audio: false,
    })
    console.log('stream', stream)
    localVideoRef.current!.srcObject = stream
    localStreamRef.current = stream
  }

  const createRtcConnection = () => {
    const _pc = new RTCPeerConnection({
      iceServers: [
        {
          urls: ['stun:stun.stunprotocol.org:3478'],
        }
      ]
    })
    _pc.onicecandidate = e => {
      if (e.candidate) {
        console.log('candidate', JSON.stringify(e.candidate))
        wsSend('candidate', JSON.stringify(e.candidate))
      }
    }
    _pc.ontrack = e => {
      remoteVideoRef.current!.srcObject = e.streams[0]
    }
    pc.current = _pc
    console.log('rtc 连接创建成功', _pc)
  }

  const createOffer = () => {
    pc.current?.createOffer({
      offerToReceiveVideo: true,
      offerToReceiveAudio: true,
    })
      .then(sdp => {
        console.log('offer', JSON.stringify(sdp))
        pc.current?.setLocalDescription(sdp)
        wsSend('offer', JSON.stringify(sdp))
        setStatus('等待对方接听')
      })
  }

  const createAnswer = () => {
    pc.current?.createAnswer({
      offerToReceiveVideo: true,
      offerToReceiveAudio: true,
    })
      .then(sdp => {
        console.log('answer', JSON.stringify(sdp))
        pc.current?.setLocalDescription(sdp)
        wsSend('answer', JSON.stringify(sdp))
        setStatus('通话中')
      })
  }

  const addLocalStreamToRtcConnection = () => {
    const localStream = localStreamRef.current!
    localStream.getTracks().forEach(track => {
      pc.current!.addTrack(track, localStream)
    })
    console.log('将本地视频流添加到 RTC 连接成功')
  }

  return (
    <div>
      <div>{`username:${username}`}</div>
      <video style={{ width: '400px' }} ref={localVideoRef} autoPlay controls></video>
      <video style={{ width: '400px' }} ref={remoteVideoRef} autoPlay controls></video>
      <br />
      <p>{`当前状态:${status}`}</p>
      <br />
      {status === '开始通话' && (
        <button onClick={createOffer}>拨号</button>
      )}
      {status === '请接听通话' && (
        <button onClick={createAnswer}>接听</button>
      )}
    </div>
  )
}

export default App

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

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

相关文章

Go网络通信

Go中HTTP协议客户端实现 Go语言标准库内置了net/http包&#xff0c;涵盖了HTTP客户端和服务端具体的实现方式。内置的net/http包提供了最简洁的HTTP客户端实现方式&#xff0c;无须借助第三方网络通信库&#xff0c;就可以直接使用HTTP中用得最多的GET和POST方式请求数据。 fun…

【Python】内置函数

文章目录 反射相关【4】基础数据类型相关【38】和数字相关&#xff08;14&#xff09;数据类型 <4>bool([x])int((x, base10)float([x])complex([real[, imag]]) 进制转换 <3>bin(x)oct(x)hex(x) 数学运算&#xff08;7&#xff09;abs(x)divmod(a, b)round(x [, n…

vue 阻止事件冒泡常用的方法

在 Vue 中&#xff0c;阻止事件冒泡有两种常用方法&#xff1a; 1. 使用 event.stopPropagation() 方法&#xff1a; 在事件处理函数中&#xff0c;可以通过调用事件对象的 stopPropagation() 方法来阻止事件冒泡。例如&#xff1a; html <template> <div click"…

00后太卷了,搞的我们这些老油条太难受了......

前几天我们公司一下子也来了几个新人&#xff0c;这些年前人是真能熬啊&#xff0c;本来我们几个老油子都是每天稍微加会班就打算走了&#xff0c;这几个新人一直不走&#xff0c;搞得我们也不好走。 2023年春招结束了&#xff0c;最近内卷严重&#xff0c;各种跳槽裁员&#x…

报表控件FastReport使用指南——使用NuGet包创建PDF文档

FastReport 是功能齐全的报表控件&#xff0c;可以帮助开发者可以快速并高效地为.NET&#xff0c;VCL&#xff0c;COM&#xff0c;ActiveX应用程序添加报表支持&#xff0c;由于其独特的编程原则&#xff0c;现在已经成为了Delphi平台最优秀的报表控件&#xff0c;支持将编程开…

多层网关已成过去,网关多合一成潮流,网关改造正当时丨Higress 正式发布 1.0 版本

作者&#xff1a;Higress 团队 01 前言 K8s 通过 Ingress / Gateway API 将网关标准化&#xff0c;逐步将安全网关、流量网关、微服务网关内聚&#xff0c;解决从单体到微服务到云原生多层网关的复杂度&#xff0c;合久必分&#xff0c;分久必合&#xff0c;多层网关已成过去…

(3)NUC980 kenerl编译

解压 用到的配置文件位置&#xff1a; /NUC980-linux-4.4.y-master/arch/arm/configs/nuc980_defconfig 执行&#xff1a; 编译linux内核源码。了解其 配置文件在 arch/arm/configs/nuc980_defconfig (1) make nuc980_defconfig 载入配置文件 (2) make menuconfig --->Devi…

SAP MM 根据采购订单反查采购申请

如何通过采购订单号查询到其前端的采购申请号。 首先从采购申请的相关报表着手&#xff0c;比如ME5A, 发现它是可以满足需求的。 例如&#xff1a;如下的采购订单&#xff0c; 该订单是由采购申请10003364转过来的。 如果想通过这个采购订单找到对应的采购申请&#xff0c;在…

Python3中goto的用法

Python3代码指定跳转可以使用goto这个库&#xff1a; 安装&#xff1a; pip install goto-statement 一般安装的版本是1.2 需要做以下修改才能正常使用&#xff1a; python 使用goto&#xff0c;遇到的问题解决_奶嘴偷走初吻的博客-CSDN博客python goto 出现报错:Attribut…

情绪管理ABC法

情绪管理ABC法 是由著名心理学家艾利斯&#xff08;Albert Ellis&#xff09;提出的一种情绪管理方法。 模型介绍 情绪&#xff0c;不取决于发生的事实&#xff0c;取决于我们如何看待这件事ABC理论认为&#xff0c;我们的情绪©&#xff0c;其实与发生的事件(A)无关&…

类的成员之:构造器(构造方法)

1.构造器的特征&#xff1a; 它具有与类相同的名称它不声明返回值类型。&#xff08;与声明为void不同&#xff09;不能被static、final、synchronized、abstract、native修饰&#xff0c;不能有return语句返回值 2.构造器的作用&#xff1a; 1.创建对象2.初始化对象的…

PID算法在流量控制中的应用

目录 增量式或位置式 目录 增量式或位置式 PID控制周期 T1 时间 T2 约4ms PID C代码 最近有小伙伴向我提问关于PID的问题&#xff1a;通过比例阀控制水流速度&#xff08;流量&#xff09;&#xff0c; 使用增量式还是位置式 PID&#xff1f;他的比例法驱动频率是500Hz…

linux环境搭建

&#x1f388;个人主页:&#x1f388; :✨✨✨初阶牛✨✨✨ &#x1f43b;推荐专栏: &#x1f354;&#x1f35f;&#x1f32f;C语言进阶 &#x1f511;个人信条: &#x1f335;知行合一 &#x1f349;本篇简介:>:介绍学习如何使用云服务器搭建Linux的环境. 前言 linux介绍…

【计算机网络中ip概念总结】【平时我们说的ip 到底是什么】【计算机网络中 ip地址是什么】

专注 效率 记忆 预习 笔记 复习 做题 欢迎观看我的博客&#xff0c;如有问题交流&#xff0c;欢迎评论区留言&#xff0c;一定尽快回复&#xff01;&#xff08;大家可以去看我的专栏&#xff0c;是所有文章的目录&#xff09;   文章字体风格&#xff1a; 红色文字表示&#…

Linux系统之编译安装python3

Linux系统之编译安装python3 一、python3介绍1. python3简介2. python3特点 二、检查本地环境1. 检查本地操作系统版本2. 检查内核版本3. 检查当前python版本 三、安装前准备工作四、下载python最新版本源码包1. 访问python官网2. 创建下载目录3. 下载python源码包4. 解压pytho…

Redis 常见面试题

1. 认识Redis Redis是一个开源的内存数据结构存储&#xff0c;Redis是一个基于内存的数据库&#xff0c;对数据的读写都在内存中完成&#xff0c;因此数据读写速度非常快&#xff0c;常用于缓存&#xff0c;分布式锁等&#xff0c;MySQL的表数据都存储在 t_order.ibd&#xff…

国内可以免费使用的GPT

一、wetab新标签页 教程&#xff1a;https://diwlwltzssn.feishu.cn/docx/MnHhdvxATomBnMxfas2cm8wWnVd 装GPT界面&#xff1a;https://microsoftedge.microsoft.com/addons/detail/wetab%E5%85%8D%E8%B4%B9chatgpt%E6%96%B0%E6%A0%87%E7%AD%BE%E9%A1%B5/bpelnogcookhocnaokfp…

华为OD机试真题(Java),跳跃游戏 II(100%通过+复盘思路)

一、题目描述 给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。 每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处: 0 <= j <= nums[i]0i + j <返回到达 nums[n - 1] 的最小跳跃…

C++学习笔记(四): 类、头文件、对象

一个类定义了一个类型&#xff0c;以及与其关联的一组操作。所谓类&#xff0c;是用户自定义的数据类型。 类机制是C最重要的特性之一。实际上&#xff0c;C最初的一个设计焦点就是能定义使用上像内置类型一样自然的类类型&#xff08;class type&#xff09;。 类的定义一般分…

Java之旅(三)

Java 输出&#xff1a;println()、print() 使用 println() 方法在 Java 中输出值或打印文本。 System.out.println("Hello World!"); println() 可以根据需要添加任意数量的方法。请注意&#xff0c;它将为每个方法添加一个新行&#xff1a; System.out.println(&…