# webrtc-chart-room **Repository Path**: Knmo/webrtc-chart-room ## Basic Information - **Project Name**: webrtc-chart-room - **Description**: 使用webrtc+websocket制作视频聊天室 - **Primary Language**: Unknown - **License**: LGPL-3.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 2 - **Created**: 2024-03-20 - **Last Updated**: 2024-03-20 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## WebRTC P2P视频会议 ### 1、为什么要用WebRTC 在介绍WebRTC之前,首先得知道为什么要使用WebRTC。我们知道HTTP是无状态的协议,每一次请求都是独立且唯一的。无状态具有一些优势,例如服务器不需要保存每一次会话的信息,不需要存储数据。但是这样每次的HTTP请求都会请求和相应冗余的数据,例如Cookie一般进行用户状态的验证。 从流程上来说,HTTP还是一种半双工的协议,浏览器请求服务,然后服务端响应浏览器的请求,每一次的请求对应的响应都不会同一时刻发生,也就意味着信息流向只能是单方向的。同时HTTP有一个缺陷,请求只能由客户端发起。这也就意味着如果我需要上传定位信息,和服务端建立心跳,例如每5秒请求一次服务端的接口,传输客户端的地理位置信息,服务端保存到数据库。而服务端无法立刻请求客户端的信息,如果需要下一次的数据,只能等待客户端的下一次请求。 为了实时获取服务端的变化和监听客户端的信息,也有过各种各样的尝试: > - 轮询:每隔一段时间,就发出一个请求,了解服务器有没有行的信息。缺点是不精确,有延时,大量冗余消息的交换 > > - 长轮询:在设定的时间内与服务端保持连接。直到服务器有新信息响应或者超时,这种方法又叫做“挂起GET”或者“搁置POST”。缺点很明显,占用服务器资源,没有标准且并没有比轮询有优势。 > - 流化技术 …… 这些方法提供了近乎实时的通信,但是都加大了对服务器的压力和不够精确的延时。 WebSocket的出现使这些问题得到了解决,但是WebSocket依然会有连接数的上线限制,最大缓冲区数据大小的限制等等。如果说我们需要例如线上的文本、图片、视频等媒体的传输,由用户A传输到用户B,那么会是下图所示的数据流向: ![image-20220901181525552](https://whcode-1304835985.cos.ap-nanjing.myqcloud.com/img/image-20220901181525552.png) ### 2、什么是WebRTC 通过服务器的数据/流传输会占用服务器的带宽,同时对服务器的内存增加压力,同时数据会受到服务器的监听,数据的隐私性会相对较差。我们希望的解决方法是,客户端A直接与客户端B进行通信,不通过服务器,这样就解决了服务器对数据的监听,同时也能够降低对服务器的压力。 WebRTC(Web Real Time Communication)全称网页实时通信,从字面上我们就能理解是为浏览器量身打造的技术。而WebRTC本身具有着多重含义,即代表着Google收购的WebRTC项目,也代表着W3C的WebRTC标准,我们统称为WebRTC技术。 回溯历史:说到端到端的通信技术,我们本能的会想到QQ,微信,这也会让我们不觉的提到音视频处理引擎Gobal IP Solutions,简称 GIPS。这是一家 1990 年成立于瑞典斯德哥尔摩的 VoIP 软件开发商,提供了可以说是世界上最好的语音引擎。 Skype、腾讯 QQ、WebEx、Vidyo 等都使用了它的音频处理引擎,包含了受专利保护的回声消除算法,适应网络抖动和丢包的低延迟算法,以及先进的音频编解码器。 Google 在 Gtalk 中也使用了 GIPS 的授权。Google 在 2011 年收购了 GIPS,并将其源代码开源,加上在 2010 年收购的 On2 获取到的 VPx 系列视频编解码器,WebRTC 开源项目应运而生,即 GIPS 音视频引擎 + 替换掉 H.264 的 VPx 视频编解码器。 在此之后,Google 又将在 Gtalk 中用于 P2P 打洞的开源项目 libjingle 融合进了 WebRTC。 所以目前 WebRTC 提供了在 Web、iOS、Android、Mac、Windows、Linux 在内的所有平台的 API,保证了 API 在所有平台的一致性。使用 WebRTC 的好处主要有以下几个方面: - 免费的使用 GIPS 先进的音视频引擎,在此之前都需要付费授权。 - 由于音视频传输是基于点对点传输的,所以实现简单的 1 对 1 通话场景,需要较少的服务器资源,借助免费的 STUN/TURN 服务器可以大大节约成本开销。 - 开发 Web 版本的应用非常方便,使用简单的 JS 接口,无需安装任何插件,即可实现音视频互通。 ### 3、WebRTC发展前景 WebRTC虽然冠以“web”之名,但并不受限于传统互联网应用或浏览器的终端运行环境。实际上无论终端运行环境是**浏览器、桌面应用、移动设备(Android或iOS)还是IoT设备**,只要IP连接可到达且符合WebRTC规范就可以互通。这一点释放了大量智能终端(或运行在智能终端上的app)的实时通信能力,打开了许多对于实时交互性要求较高的应用场景的想象空间,譬如在线教育、视频会议、视频社交、远程协助、远程操控等等都是其合适的应用领域。 全球领先的技术研究和咨询公司Technavio最近发布了题为“全球网络实时通讯(WebRTC)市场,2017-2021”的报告。报告显示,2017-2021年期间,全球网络实时通信(WebRTC)市场将以**34.37%的年均复合增长率增长**,增长十分迅速。增长主要来自北美、欧洲及亚太地区。 ### 4、P2P音视频通信原理 首先思考的问题:两个不同网络环境的(具备摄像头/麦克风多媒体设备的)浏览器,要实现点对点 的实时音视频对话,难点在哪里? #### 4.1 媒体协商 彼此要了解对方支持的媒体格式。比如:Peer-A端可支持VP8、H264多种编码格式,而Peer-B端支持VP9、H264,要保证二端都正确的编解码,**最简单的办法就是取它们的交集H264** 注:**有一个专门的协议 ,称为Session Description Protocol (SDP)**,可用于描述上述这类信息,在WebRTC中,参与视频通讯的双方必须先交换SDP信息,这样双方才能知根知底,而交换SDP的过程,也称为"媒体协商"。 ![image-20220901202550460](https://whcode-1304835985.cos.ap-nanjing.myqcloud.com/img/image-20220901202550460.png) #### 4.2 网络协商 彼此要了解对方的网络情况,这样才有可能找到一条相互通讯的链路 先说结论:(1)获取外网IP地址映射;(2)通过信令服务器(signal server)交换"网络信息" 理想的网络情况是每个浏览器的电脑都是私有公网IP,可以直接进行点对点连接。 实际情况是:我们的电脑和电脑之前或大或小都是在某个局域网中,需要NAT(Network Address Translation,网络地址转换) 在解决WebRTC使用过程中的上述问题的时候,我们需要用到**STUN和TURN**。 **STUN** STUN(Session Traversal Utilities for NAT,NAT会话穿越应用程序)是一种网络协议,它允许位于NAT(或多重NAT)后的客户端找出自己的公网地址,查出自己位于哪种类型的NAT之后以及NAT为某一个本地端口所绑定的Internet端端口。这些信息被用来在两个同时处于NAT路由器之后的主机之间创建UDP通信。该协议由RFC 5389定义。 在遇到上述情况的时候,我们可以建立一个STUN服务器,这个服务器做什么用的呢?主要是给无法在公网环境下的视频通话设备分配公网IP用的。这样两台电脑就可以在公网IP中进行通话。不过一般情况我们可以用大厂的服务器。 例如谷歌:stun2.l.google.com:19302 使用一句话说明STUN做的事情就是:告诉我你的公网IP地址+端口是什么。搭建STUN服务器很简单,媒体流传输是按照P2P的方式。 那么问题来了,STUN并不是每次都能成功的为需要NAT的通话设备分配IP地址的,P2P在传输媒体流时,使用的本地带宽,在多人视频通话的过程中,通话质量的好坏往往需要根据使用者本地的带宽确定。那么怎么办?TURN可以很好的解决这个问题。 **TURN** TURN的全称为Traversal Using Relays around NAT,是STUN/RFC5389的一个拓展,主要添加了Relay功能。如果终端在NAT之后, 那么在特定的情景下,有可能使得终端无法和其对等端(peer)进行直接的通信,这时就需要公网的服务器作为一个中继, 对来往的数据进行转发。这个转发的协议就被定义为TURN。 在STUN分配公网IP失败后,可以通过TURN服务器请求公网IP地址作为中继地址。这种方式的带宽由服务器端承担,在多人视频聊天的时候,本地带宽压力较小,并且,根据Google的说明,TURN协议可以使用在所有的环境中。(单向数据200kbps 一对一通话) 以上是WebRTC中经常用到的2个协议,STUN和TURN服务器我们使用coturn开源项目来搭建。 补充:ICE跟STUN和TURN不一样,ICE不是一种协议,而是一个框架(Framework),它整合了STUN和TURN。coturn开源项目集成了STUN和TURN的功能。 在WebRTC中用来描述 网络信息的术语叫candidate。 即是: 媒体协商 sdp 网络协商 candidate #### 4.3 媒体协商+网络协商数据的交换通道 从上面1/2点我们知道了2个客户端协商媒体信息和网络信息,那怎么去交换?是不是需要一个中间商去做交换?所以我们需要一个信令服务器(Signal server)转发彼此的媒体信息和网络信息。 我们在基于WebRTC API开发应用(APP)时,可以将彼此的APP连接到信令服务器(Signal Server,一般搭建在公网,或者两端都可以访问到的局域网),借助信令服务器,就可以实现上面提到的SDP媒体信息及Candidate网络信息交换。 信令服务器不只是交互 媒体信息sdp和网络信息candidate,比如: - 房间管理 - 人员进出房间 为了简化,我们在Demo中统一的使用websocket进行数据的转发,替代信令服务器的作用 ![image-20220901203119115](https://whcode-1304835985.cos.ap-nanjing.myqcloud.com/img/image-20220901203119115.png) ### 5、基于浏览器一个简单的1对1通信 user1 ```js const lc = new RTCPeerConnection() const dc = lc.createDataChannel("channel") dc.onmessage = e => console.log("Just got a message " + e.data); dc.onopen = e => console.log("Connection opened!") lc.onicecandidate = e => console.log("New Ice Candidate! reprinting SDP" + JSON.stringify(lc.localDescription)) lc.createOffer().then(o => lc.setLocalDescription(o)).then(a => console.log("set Successlly")) /* ---- 拿到接收方sdp后 ---- */ const answer = {/*接受方的sdp*/} lc.setRemoteDescription(answer) ``` user2 ```js const offer = {/*发送方的sdp*/} const rc = new RTCPeerConnection() rc.onicecandidate = e => console.log("New Ice Candidate! reprinting SDP" + JSON.stringify(rc.localDescription)) rc.ondatachannel = e => { rc.dc = e.channel; rc.dc.onmessage = e => console.log("new message from client! " + e.data); rc.dc.onopen = e => console.log("Connection OPENED!!!!") } rc.setRemoteDescription(offer).then(a => console.log("offer set!")) rc.createAnswer().then(a => rc.setLocalDescription(a)).then(a => console.log("answer created")); ``` ![image-20220901210633449](https://whcode-1304835985.cos.ap-nanjing.myqcloud.com/img/image-20220901210633449.png) ### 6、WebRTC 常用 API 解析 WebRTC主要让浏览器具备三个作用。 > - MediaStream 获取音频和视频 > - RTCPeerConnection 进行音频和视频通信 > - RTCDataChannel 进行任意数据的通信 #### 6.1 MediaStream `MediaStream.getAudioTracks()` - : 返回流中 kind 属性为"audio"的[`MediaStreamTrack`](https://developer.mozilla.org/zh-CN/docs/Web/API/MediaStreamTrack)列表。顺序是不确定的,不同浏览器间会有不同,每次调用也有可能不同。 `MediaStream.getTracks()` - : 返回流中所有的[`MediaStreamTrack`](https://developer.mozilla.org/zh-CN/docs/Web/API/MediaStreamTrack)列表。 `MediaStream.addTrack()` - : 存储传入参数 [`MediaStreamTrack`](https://developer.mozilla.org/zh-CN/docs/Web/API/MediaStreamTrack) 的一个副本。如果这个轨道已经被添加到了这个媒体流,什么也不会发生; 如果目标轨道为“完成”状态(也就是已经到尾部了),一个INVALID_STATE_RAISE 异常会产生。 `MediaStream.id` - 这是一个包含 36 个字符的 [`DOMString`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String) ,用来作为这个对象的唯一标识符 (GUID) 。 `MediaStream.getTrackById()` (en-US) - : 返回给定 ID 的轨道。如果没有参数或者没有指定 ID 的轨道,将返回 null。如果有几个轨道有同一个 ID,将返回第一个。 `MediaStream.removeTrack()` - : 移除作为参数传入的 [`MediaStreamTrack`](https://developer.mozilla.org/zh-CN/docs/Web/API/MediaStreamTrack)。 如果这个轨道不在`MediaStream 对象中什么也不会发生;` 如果目标轨道为“完成”状态,一个 INVALID_STATE_RAISE 异常会产生。 #### 6.2 RTCPeerConnection `RTCPeerConnection.localDescription` 只读属性,返回一个 [`RTCSessionDescription`](https://developer.mozilla.org/zh-CN/docs/Web/API/RTCSessionDescription) ,它描述了这条连接的本地端的会话控制(用户会话所需的属性以及配置信息)。如果本地的会话控制还没有被设置,它的值就会是 null。 `RTCPeerConnection.connectionState` 只读 connectionState 属性通过返回由枚举 RTCPeerConnectionState 指定的字符串值之一来指示对等连接的当前状态。 `RTCPeerConnection.onaddstream` **即将弃用(新见onaddtrack)** 是收到`addstream` 事件时调用的事件处理器。 Such an event is 当[`MediaStream`](https://developer.mozilla.org/zh-CN/docs/Web/API/MediaStream) 被远端机器添加到这条连接时,该事件会被触发。 当调用[`RTCPeerConnection.setRemoteDescription()`](https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection/setRemoteDescription)方法时,这个事件就会被立即触发,它不会等待 SDP 协商的结果。 `RTCPeerConnection.onicecandidate` 是收到 `icecandidate` 事件时调用的事件处理器.。当一个 [`RTCICECandidate` (en-US)](https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate) 对象被添加时,这个事件被触发。 `RTCPeerConnection.createOffer()` 生成一个 offer,它是一个带有特定的配置信息寻找远端匹配机器(peer)的请求。这个方法的前两个参数分别是方法调用成功以及失败的回调函数,可选的第三个参数是用户对视频流以及音频流的定制选项(一个对象)。 `RTCPeerConnection.createAnswer()` 在协调一条连接中的两端 offer/answers 时,根据从远端发来的 offer 生成一个 answer。这个方法的前两个参数分别是方法调用成功以及失败时的回调函数,可选的第三个参数是生成的 answer 的可供选项。 `RTCPeerConnection.setLocalDescription()` 改变与连接相关的本地描述。这个描述定义了连接的属性,例如:连接的编码方式。连接会受到它的改变的影响,而且连接必须能同时支持新的以及旧的描述。这个方法可以接收三个参数,一个[`RTCSessionDescription`](https://developer.mozilla.org/zh-CN/docs/Web/API/RTCSessionDescription) 对象包含设置信息,还有两个回调函数,它们分别是方法调用成功以及失败的回调函数。 `RTCPeerConnection.setRemoteDescription()` 改变与连接相关的远端描述。这个描述定义了连接的属性,例如:连接的编码方式。连接会受到它的改变的影响,而且连接必须能同时支持新的以及旧的描述。这个方法可以接收三个参数,一个[`RTCSessionDescription`](https://developer.mozilla.org/zh-CN/docs/Web/API/RTCSessionDescription) 对象包含设置信息,还有两个回调函数,它们分别是方法调用成功以及失败的回调函数。 `RTCPeerConnection.createDataChannel()` 在一条连接上建立一个新的[`RTCDataChannel`](https://developer.mozilla.org/zh-CN/docs/Web/API/RTCDataChannel)(用于数据发送)。这个方法把一个数据对象作为参数,数据对象中包含必要的配置信息。 `RTCPeerConnection.close()` 关闭一个 RTCPeerConnection 实例所调用的方法。 #### 6.3 RTCDataChannel `RTCDataChannel.id` 当[`RTCDataChannel`](https://developer.mozilla.org/zh-CN/docs/Web/API/RTCDataChannel)对象被创建出来的时候,返回一个无符号 short 类型的数据,作为通道的标识 id。 `RTCDataChannel.readyState` 返回枚举类型的 RTCDataChannelState,表示数据连接的状态,有以下几种类型: - `"connecting"` 该状态表示底层链路还未建立和激活,该状态还是由[`RTCPeerConnection.createDataChannel()`](https://developer.mozilla.org/zh-CN/docs/Web/API/RTCPeerConnection/createDataChannel)生成的 datachannel 初始状态。 - `"open"` 该状态表示底层链路已经连接成功并且运行。这个状态还是由[`RTCDataChannelEvent` (en-US)](https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannelEvent)分发的 datachannel 的初始状态。 - `"closing"` 该状态表示底层链路已经在关闭的过程中。该状态下将不会接受新的发送任务,但是缓冲队列中的消息还是会被继续发送或者接收。 - `"closed"` 该状态表示底层链路已经完全被关闭(或者无法处于 established 状态)。 `RTCDataChannel.onopen` 当接收到`open` 事件时的事件处理器,当底层链路数据传输成功,端口状态处于 established 的时候会触发该事件。 `RTCDataChannel.onmessage` 当接收到`message`事件时的事件处理器。当有数据被接收的时候会触发该事件。 `RTCDataChannel.onclose` 当接收到`close`事件时候的事件处理器。当底层链路被关闭的时候会触发该事件。 `RTCDataChannel.onerror` 当接收到`error (en-US)` 事件时候的事件处理器。当遇到错误的时候会触发该事件。 `RTCDataChannel.close()` 关闭 channel 的方法。这个关闭动作不是直接生效的。这个方法会将 channel 的[`state` (en-US)](https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/readyState) 属性设置为`"closing"`状态,在消息队列中的消息全部发送完毕之后,channel 才会被关闭。 `RTCDataChannel.send()` 将参数中的数据通过 channel 发送。这个数据可以是[`DOMString`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String), [`Blob`](https://developer.mozilla.org/zh-CN/docs/Web/API/Blob), [`ArrayBuffer`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer)或者是 [`ArrayBufferView`](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/TypedArray)类型。 ### 7、项目简介 此项目采用前后端分离(前端VUE,后端Springboot)的方式进行开发,通过websocket传输sdp文本,使两端浏览器之间建立对等连接。数据走向如下图所示: ![image-20220902093000350](https://whcode-1304835985.cos.ap-nanjing.myqcloud.com/img/image-20220902093000350.png) 服务器使用websocket建立客户端A与客户端B之间的通信连接,在连接后传输SDP文本,建立客户端A到客户端B之间的连接。 整体项目功能由三个部分组成: > 文本通信:客户端A与客户端可通过文本的方式进行通信,类似于聊天场景 > > 音视频通话:通过WebRTC API进行摄像头调用,音频的录制并进行传输 > > 画板:可共享画板显示,通过笔迹数组的长度变化进行Canvas截图传输 ![](https://whcode-1304835985.cos.ap-nanjing.myqcloud.com/img/image-20220902093241114.png) ![image-20220902093901095](https://whcode-1304835985.cos.ap-nanjing.myqcloud.com/img/image-20220902093901095.png) ### 参考资料 [`WebRTC API MDN`](https://developer.mozilla.org/zh-CN/docs/Web/API/WebRTC_API) [`超详细WebRTC教程`](https://www.bilibili.com/video/BV1Wi4y1N74y?spm_id_from=333.1007.top_right_bar_window_history.content.click&vd_source=c56cdea67dfeafa47618e1ade64859a2) [`中文版 WebRTC 1.0官方文档`](https://github.com/RTC-Developer/WebRTC-Documentation-in-Chinese) [`WebRTC入门与提高`](https://zhuanlan.zhihu.com/p/93107411) [`为何一直推荐WebRTC?`](https://www.zhihu.com/question/50277029/answer/2540305500)