WebRTC入门
一、协议
1.1 P2P 通信原理与实现
1.1.1 基本术语
防火墙(Firewall): 防火墙主要限制内网和公网的通讯,通常丢弃未经许可的数据包。防火墙会检测(但是不修改)试图进入内网数据包的 IP 地址和 TCP/UDP 端口信息。
网络地址转换协议(NAT): 用来给你的(私网)设备映射一个公网的 IP 地址的协议。一般情况下,路由器的 WAN 口有一个公网 IP,所有连接这个路由器 LAN 口的设备会分配一个私有网段的 IP 地址(例如 192.168.1.3)。私网设备的 IP 被映射成路由器的公网 IP 和唯一的端口,通过这种方式不需要为每一个私网设备分配不同的公网 IP,但是依然能被外网设备发现。NAT 不止检查进入数据包的头部,而且对其进行修改,从而实现同一内网中不同主机共用更少的公网 IP(通常是一个)。
基本 NAT(Basic NAT): 基本 NAT 会将内网主机的 IP 地址映射为一个公网 IP,不改变其 TCP/UDP 端口号。基本 NAT 通常只有在当 NAT 有公网 IP 池的时候才有用。
网络地址-端口转换器(NAPT): 到目前为止最常见的即为 NAPT,其检测并修改出入数据包的 IP 地址和端口号,从而允许多个内网主机同时共享一个公网 IP 地址。
锥形 NAT(Cone NAT): 在建立了一对(公网 IP,公网端口)和(内网 IP,内网端口)二元组的绑定之后,Cone NAT 会重用这组绑定用于接下来该应用程序的所有会话(同一内网 IP 和端口),只要还有一个会话还是激活的。 例如,假设客户端 A 建立了两个连续的对外会话,从相同的内部端点(10.0.0.1:1234)到两个不同的外部服务端 S1 和 S2。Cone NAT 只为两个会话映射了一个公网端点(155.99.25.11:62000), 确保客户端端口的“身份”在地址转换的时候保持不变。由于基本 NAT 和防火墙都不改变数据包的端口号,因此这些类型的中间件也可以看作是退化的 Cone NAT。
1 | Server S1 Server S2 |
1.1.2 UDP 打洞(UDP hole punching)
P2P 通信技术中被广泛采用的技术“UDP 打洞”。UDP 打洞技术依赖于通常防火墙和 cone NAT 允许正当的 P2P 应用程序在中间件中打洞且与对方建立直接链接的特性。
在学习 UDP 打洞之前,我们先了解一下另外两种 P2P 通信技术。
(1)中继(Relaying)
中继是最可靠但效率最低的一种 P2P 通信技术,它的原理是通过一台服务器来中继转发不同客户端的数据。
1 | Server S |
什么意思呢?就是我和你开视频,我和你的视频数据会直接被我们共同连接上的一台服务器接收,这台服务器会将你我的视频数据分别转发响应给我和你的客户端。这样服务器压力就很大,带宽需求也非常大,当仅仅只有两个客户端连接服务器开视频的话,服务器的带宽就至少是客户端带宽的两倍,CPU 消耗同样也是。那么当同时视频通话的人很多了,那么服务器的压力难以想象。
所以中继是一种效率很低的 P2P 通信技术。
(2)逆向连接(Connection reversal)
这种连接只有在两个通信端点中有一个不存在中间件的时候有效。
例如,客户端 A 在 NAT 之后而客户端 B 拥有全局 IP 地址,如下图:
1 | Server S |
客户端 A 内网地址为 10.0.0.1,且应用程序正在使用 TCP 端口 1234。A 和服务器 S 建立了一个连接,服务器的 IP 地址为 18.181.0.31,监听 1235 端口。NAT A 给客户端 A 分配了 TCP 端口 62000,地址为 NAT 的公网 IP 地址 155.99.25.11, 作为客户端 A 对外当前会话的临时 IP 和端口。因此 S 认为客户端 A 就是 155.99.25.11:62000。而 B 由于有公网地址,所以对 S 来说 B 就是 138.76.29.7:1234。
当客户端 B 想要发起一个对客户端 A 的 P2P 链接时,要么链接 A 的外网地址 155.99.25.11:62000,要么链接 A 的内网地址 10.0.0.1:1234,然而两种方式链接都会失败。 链接 10.0.0.1:1234 失败自不用说,为什么链接 155.99.25.11:62000 也会失败呢?来自 B 的 TCP SYN 握手请求到达 NAT A 的时候会被拒绝,因为对 NAT A 来说只有外出的链接才是允许的。
在直接链接 A 失败之后,B 可以通过 S 向 A 中继一个链接请求,从而从 A 方向“逆向“地建立起 A-B 之间的点对点链接。
现在很多 P2P 系统都实现了这种技术,但是这种技术有局限性,只有当其中一放客户端有公网 IP 的时候才能建立起连接。为什么现在很多 P2P 系统都实现了逆向连接技术,因为我们接下来要讲的 UDP 打洞技术,主要是依赖这种技术。
UDP 打洞正文开始:
现在最多的网路连接情况是双方都是在内网下,都需要通过 NAT 进行地址转换,所以上面的逆向连接不适用,但是可以利用逆向连接技术进行改造。
假设客户端 A 和客户端 B 的地址都是内网地址,且在不同的 NAT 后面。A、B 上运行的 P2P 应用程序和服务器 S 都使用了 UDP 端口 1234,A 和 B 分别初始化了 与 Server 的 UDP 通信,地址映射如图所示:
1 | Server S |
现在假设客户端 A 打算与客户端 B 直接建立一个 UDP 通信会话。如果 A 直接给 B 的公网地址 138.76.29.7:31000 发送 UDP 数据,NAT B 将很可能会无视进入的 数据(除非是 Full Cone NAT),因为源地址和端口与 S 不匹配,而最初只与 S 建立过会话。B 往 A 直接发信息也类似。
假设 A 开始给 B 的公网地址发送 UDP 数据的同时,给服务器 S 发送一个中继请求,要求 B 开始给 A 的公网地址发送 UDP 信息。
A 往 B 的输出信息会导致 NAT A 打开 一个 A 的内网地址与与 B 的外网地址之间的新通讯会话,B 往 A 亦然。一旦新的 UDP 会话在两个方向都打开之后,客户端 A 和客户端 B 就能直接通讯, 而无须再通过引导服务器 S 了。
UDP 打洞技术有许多有用的性质。一旦一个的 P2P 链接建立,链接的双方都能反过来作为“引导服务器”来帮助其他中间件后的客户端进行打洞, 极大减少了服务器的负载。应用程序不需要知道中间件具体是什么(如果有的话),因为以上的过程在没有中间件或者有多个中间件的情况下 也一样能建立通信链路。
还有一些特殊情况:当通信双方都在同一局域网,也就是两个客户端都在一个内网下呢?是不是可以降低 NAT 转换,直接在内网上连接呢?此外还有,当一些大型企业,内网中有多级 NAT 转换呢?这里已不再本文的讨论中了,详细可以看以下参考文章详细了解:
学到这里,根据上面的原理是可以实现自己的一套程序和通信规则,但很多时候是需要对接第三方的协议,往往这个适配是比较麻烦的。因此就产生了标准化的通用规则(STUN、TURN、ICE),下面的几个章节将逐个介绍这些协议。
1.2 STUN 协议
STUN(STUN/RFC3489(废弃),STUN/RFC5389)是 P2P 标准化通信规则(协议)之一。
1.2.1 简介
NAT 的会话穿越功能Session Traversal Utilities for NAT (STUN) (缩略语的最后一个字母是 NAT 的首字母)是一个允许位于 NAT 后的客户端找出自己的公网地址,判断出路由器阻止直连的限制方法的协议。
STUN 是一个 C/S 架构的协议,支持两种传输类型。一种是请求/响应(request/respond)类型,由客户端给服务器发送请求,并等待服务器返回响应;另一种是指示类型(indication transaction),由服务器或者客户端 发送指示,另一方不产生响应。对于请求/响应类型,允许客户端将响应和产生响应的请求连接起来; 对于指示类型,通常在 debug 时使用。我们主要了解请求/响应类型。
1.2.2 通信过程
客户端通过给公网的 STUN 服务器发送请求获得自己的公网地址信息,以及是否能够被(穿过路由器)访问。
- 客户端 A 向服务器产生一个 Request(STUN 叔叔,你能告诉我我的 ip 是多少吗)
- 服务器接收 Request,检查报文是否合法,并生成 Success 响应或 Error 响应(A 小朋友,你的 ip 是 208.141.55.130:3255)
1.3 TURN 协议
TURN(TURN/RFC5766)是 P2P 标准化通信规则(协议)之一,是对 STUN 的补充。
1.3.1 简介
TURN 的全称为Traversal Using Relays around NAT (TURN) ,是 STUN/RFC5389 的一个拓展,主要添加了 Relay 功能。前面介绍的 STUN 协议处理的是市面上大多数的 Cone NAT,但还有少量的设备使用的 Symmetric NAT。因此传统的打洞方法不适用,为了保证这一部分设备能够建立通信,我们不得不通过中继(Relaying)的方法进行连接,这时就需要公网的服务器作为一个中继, 对来往的数据进行转发。这个转发的协议就被定义为 TURN。这种情况会增加服务器负担,所以这是最坏的情况的通信解决方案。
TURN 服务器与客户端之间的连接都是基于 UDP 的,但是服务器和客户端之间可以通过其他各种连接来传输 STUN 报文, 比如 TCP/UDP/TLS-over-TCP。客户端之间通过中继传输数据时候,如果用了 TCP,也会在服务端转换为 UDP,因此建议客户端使用 UDP 来进行传输。至于为什么要支持 TCP,那是因为一部分防火墙会完全阻挡 UDP 数据,而对于三次握手的 TCP 数据则不做隔离。
1.3.2 通信过程
客户端 A 向 STUN 服务器发送请求获取自己的公网地址,STUN 服务器可以获取到客户端 A 的地址,但发现客户端 A 的使用的 Symmetric NAT,因此 STUN 服务器告诉客户端 A,我不能帮助你和客户端 B 建立连接,你们之间可以通过 TURN 进行连接。因此客户端 A 和客户端 B 同时去连接 TURN 服务器,通过 TURN 服务器进行中继连接。
- 客户端 A 向 STUN 服务器产生一个 Request(STUN 叔叔,你能告诉我我的 ip 是多少吗)
- STUN 服务器响应(A 小朋友,你的 ip 是 208.141.55.130:3255,可是你的 ip 别人不能和你连接哦,你需要去找你 TURN 大伯,他是专门负责帮你连接)
- 客户端 A 向 TURN 服务器发起请求(TURN 大伯,STUN 叔叔叫我来找你)
- TURN 服务器响应(A 小侄儿,我知道了,但是现在还没有其他小朋友找你哦,你可以在这附近逛一逛,每 10 分钟要给我报告一下你还在这附近哦,一有其他小朋友来找你我就通知你。)
1.4 ICE 协议
TURN(ICE/RFC5245)是 P2P 标准化通信规则(协议)之一,提供了完整的 NAT 传输解决方案。
STUN、TURN 都是工具类协议,只提供穿透 NAT 的功能。且 TURN 本身就是被设计为 ICE/RFC5245 的一部分
1.4.1 简介
ICE 的全称为Interactive Connectivity Establishment (ICE),即交互式连接建立。在实际的网络当中,有很多原因能导致简单的从 A 端到 B 端直连不能如愿完成。这需要绕过阻止建立连接的防火墙,给你的设备分配一个唯一可见的地址(通常情况下我们的大部分设备没有一个固定的公网地址),如果路由器不允许主机直连,还得通过一台服务器转发数据。ICE 通过使用 STUN、TURN、NAT、SDP 技术完成上述工作。(引用自:https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Protocols)
ICE 是一个用于在Offer/Answer模式下的 NAT 传输协议,主要用于 UDP 下多媒体会话的建立,其使用了 STUN 协议以及 TURN 协议,同时也能被其他实现了 Offer/Answer 模型的的其他程序所使用,比如SIP(Session Initiation Protocol)。
网络编程的 ICE(Internate Communications Engine):是一种用于分布式程序设计的网络通信中间件,本文指并非此 ICE
交互式连接 ICE(Interactive Connectivity Establishment):是一个允许你的浏览器和对端浏览器建立连接的协议框架。
1.4.2 SDP 会话描述
ICE 信息的描述格式通常采用标准的SDP,其全称为Session Description Protocol (SDP) ,即会话描述协议。SDP 不是一个真正的协议,而是一种数据格式,用于描述在设备之间共享媒体的连接。可以被其他传输协议用来交换必要的信息,如 SIP 和 RTSP 等。
SDP 格式:
SDP 由一行或多行 UTF-8 文本组成,每行以一个字符的类型开头,后跟等号(“ =”),然后是包含值或描述的结构化文本,其格式取决于类型。
SDP 会话描述包含了多行如下类型的文本:
1 | <type>=<value> |
以给定字母开头的文本行通常称为“字母行”。例如,提供媒体描述的行的类型为“ m”,因此这些行称为“ m 行”。
1 | m=audio 49170 RTP/AVP 0 |
<type>是大小写敏感的,其中一些行是必须要有的,有些是可选的,所有元素都必须以固定顺序给出。如下所示,其中可选的元素标记为* :
1 | 会话描述: |
所有元素的 type 都为小写,并且不提供拓展.但是我们可以用 a(attribute)字段来提供额外的信息。一个 SDP 描述的例子如下:
1 | v=0 |
具体字段的 type/value 描述和格式可以参考RFC4566。
1.4.3 Offer/Answer 模型
SDP 用来描述多播主干网络的会话信息,但是并没有具体的交互操作细节是如何实现的,因此RFC3264 定义了一种基于 SDP 的 Offer/Answer 模型。
在该模型中,会话参与者的其中一方生成一个 SDP 报文构成 offer, 其中包含了一组 offer 希望使用的多媒体流和编解码方法,以及 offer 用来接收改数据的 IP 地址和端口信息。
offer 传输到会话的另一端(称为 answer),由这一端生成一个 answer,即用来响应对应 offer 的 SDP 报文。
answer 中包含不同 offer 对应的多媒体流,并指明该流是否可以接受。
1.4.4 ICE 工作流程
一个典型的 ICE 工作环境如下,有两个端点 A 和 B,都运行在各自的 NAT 之后(他们自己也许并不知道),NAT 的类型和性质也是未知的。L 和 R 通过交换 SDP 信息在彼此之间建立多媒体会话,通常交换通过一个 SIP 服务器完成:
1 | +-----------+ |
ICE 的基本思路是,每个终端都有一系列传输地址(包括传输协议,IP 地址和端口)的候选,可以用来和其他端点进行通信。其中可能包括:
- 直接和网络接口联系的传输地址(host address)
- 经过 NAT 转换的传输地址,即反射地址(server reflective address)
- TURN 服务器分配的中继地址(relay address)
通过之前的学习,我们可以了解到每个终端的情况是比较复杂的(有的终端可能同时连着 wifi 和网线,有多个内网地址),所有每个终端有多种可以连接的方案。
获取到这一系列传输地址后,会以一定优先级将地址排序。按照优先级和其他终端的传输地址进行组合检测连接可用性(连接性检查:Connectivity Checks)。
两端连接性检查,是一个 4 次握手过程:
1 | A B |
连接性检查详细过程:
为中继候选地址生成许可(Permissions);
从本地候选往远端候选发送 Binding Request:
在 Binding 请求中通常需要包含一些特殊的属性,以在 ICE 进行连接性检查的时候提供必要信息:
- PRIORITY 和 USE-CANDIDATE:优先级和候选
- ICE-CONTROLLED 和 ICE-CONTROLLING:标识本端是受控方还是主控方(offer 生成方)。
- 生成 Credential:STUN 短期身份验证
处理 Response:
当收到 Binding Response 时,终端会将其与 Binding Request 相联系,通常生成事务 ID。随后将会将此事务 ID 与候选地址对进行绑定。
- 成功响应:要同时满足三个条件(STUN 传输产生一个 Success Response;response 的源 IP 和端口等于 Binding Request 的目的 IP 和端口;response 的目的 IP 和端口等于 Binding Request 的源 IP 和端口)
- 失败响应:487 错误,并将检测地址状态设置为 Waiting
以上仅对协议作了简单的介绍,具体服务器程序实现可参考:https://github.com/evilpan/TurnServer
1.5 经典 WebRTC 连接建立流程
通过前面的协议了解学习,相信大家已经对 WebRTC 的底层连接流程有了一个模糊的意思,这里有张图展现了具体的连接流程。
引用自:https://aggresss.blog.csdn.net/article/details/106832965
二、服务器搭建
2.1 STUN/TURN 服务器
STUN 服务器已有现成项目:https://github.com/coturn/coturn
以下是在 ubuntu 上的安装和配置:
2.1.1 安装 coturn
可以克隆 github 上的源码编译安装,在 ubuntu 里有直接的安装包
1 | apt-get -y update |
安装完毕后,先关闭 coturn 服务:
1 | systemctl stop coturn |
2.1.2 配置 coturn
(1) 允许 turnserver
首先需要允许 turnserver,打开/etc/default/coturn
文件,将注释去掉:
1 | vim /etc/default/coturn |
取消注释后如下:
1 | TURNSERVER_ENABLED=1 |
(2) 获取 ip 和 SSL
首选需要获取一下自己的内网 ip 以及网卡:
1 | ifconfig |
生成 SSL 证书:
1 | apt install openssl |
1 | openssl req -x509 -newkey rsa:2048 -keyout /etc/turn_server_pkey.pem -out /etc/turn_server_cert.pem -days 99999 -nodes |
(3) 配置
接下来正式改配置文件/etc/turnserver.conf
,改之前先将原文件备份一个:
1 | mv /etc/turnserver.conf /etc/turnserver.conf.bat |
然后新建配置文件:
1 | vim /etc/turnserver.conf |
然后复制以下配置:
1 | server-name=turn.webrtc.zzboy.cn |
2.1.3 测试
工具:Trickle ICE
点击打开上面的工具
2.2 Nodejs 构建信令服务器(Signal Server)
信令服务器我直接使用的一个开源项目:https://github.com/qdgx/WebRtcRoomServer
其实信令服务器已经涉及到实战了,这里就不讲具体实现,这里只先部署。
单纯地看,信令服务器其实可以算作是一个后端项目,我们这里部署也只是对该项目进行服务器部署。这里我使用的这个开源项目是使用 node.js 开发的,因此部署步骤和 node.js 部署步骤相差无异。
以下是我在 ubuntu 上的安装和配置:
2.2.1 安装 node 环境
(1) 更新环境,安装 curl、git
1 | apt-get update |
(2) 安装 node.js
先去官网https://nodejs.org/,查看最新稳定长期支持版,发现最新稳定版是14.15.3 LTS,node.js 的每个大版本号都有相对应的源,比如这里的 14.15.3 版本的源是 https://deb.nodesource.com/setup_14.x
所以在终端执行:
1 | curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash - |
然后安装 node.js
1 | apt-get install nodejs |
node -v
和 npm -v
查看 node 和 npm 是否安装成功
2.2.2 克隆项目,安装依赖
进入用户目录,克隆项目:
1 | cd ~/ && git clone https://github.com/qdgx/WebRtcRoomServer.git |
安装依赖:
1 | cd ~/WebRtcRoomServer |
启动服务:
1 | node app.js |
在浏览器打开以下地址,测试一下是否访问:
只要浏览器提示该页面存在风险,即表示项目已生效,点击高级,选择接受风险继续访问即可。(为什么提示风险:因为这个项目的证书是自签名证书)
如果无法访问,请检查服务器安全组是否打开了 TCP 和 UDP 协议的 8443 端口,有些服务器开端口需要在服务器上那配置安全组,比如阿里云 ECS 和华为云。
2.2.3 pm2 管理 node 服务
直接用node app.js
运行项目,在关闭终端后,node 项目也会随之被关闭,因此需要使用额外的工具来保持 node 服务一直开启。
安装 pm2:
1 | npm install pm2@latest -g |
启动服务:
1 | pm2 start app.js --name signal-server --watch |
- name:给应用命名,可以不管
- watch:相当于热更新,应用文件更新后会重启应用
有关 pm2 的使用,可以百度查询一下,也可以参考本人之前写的一篇文章:https://www.zzboy.cn/Learning/f360ef90efef
三、API 学习
以下主要介绍下一章节实战开中需要到的常用接口,完整的接口学习可查看对应官方文档。
3.1 socket.io
中文 w3chool:https://www.w3cschool.cn/socket/
Socket
是一种全双工通信,当客户端和服务端建立起连接后,如果不主动断开,双方可以一直互相发送消息,适合于双方频繁通信的场景,也是支持服务端主动推送的一种通信方式。WebSocket
是Html5
推出的前端可以直接使用的 API,不过目前项目中用的还是 socket.io 比较多。socket.io 在浏览器环境下封装了 WebSocket, 可以给开发者带来更好的体验,在功能上也更完善。
socket.io 主要使用两个方法:
emit(description: string, data: any
:监听事件;description
是标识;data
是需要发送的数据。on(description: string, callback: function
:监听事件;description
表示监听的标识;callback
是监到事件后处理方法,参数是emit
发送的数据。
通俗说,一个就是发送,一个是接收。发送方法需要指定谁(description
)来接收;接收方法找到对应description
接收。
3.1.1 服务器端
(1) 安装
1 | npm install socket.io |
(2) 初始化
1 | const httpServer = require("http").createServer(); // 创建http服务 |
配置项:是初始配置 socket.io 的一些参数,我们使用默认的接口,如需要配置,可以看文档了解具体配置项:https://socket.io/docs/v3/server-api/#new-Server-httpServer-options
根据 WebRTC 安全策略,我们需要使用 https,因此,比较完整的初始化代码为:
1 | const fs = require("fs"); |
(3) 方法
io.on(‘connection’, fn) :监听客户端连接
从上面初始化代码不难看出,socket.io 第一个方法应该io.on('connection', fn)
。
connection 是保留description
,当有客户端连接上当前服务器时,就会触发。
我们需要在其回调中处理相关业务:
1 | io.on("connection", (socket) => { |
socket.on(‘disconnect’, fn) :监听客户端断开连接
1 | socket.on("disconnect", (reason) => { |
socket.emit() : 发送信息
3.1.2 客户端
3.2 音视频相关 API
3.2.1 navigator.mediaDevices
浏览器 API,可以通过该浏览器 API 获取用户媒体设备,通常只会用到一个方法:getUserMedia(options)
,调用该方法时,浏览器会弹出请求音频或视频的权限,用户同意授权过后,即可获取到音视频流。
1 | navigator.mediaDevices |
需要注意:navigator
的mediaDevices
属性需要在 https 环境下才会有,这是浏览器的限制。
options: 配置项
一般可直接设置为:{ audio: true, video: true }
,表示为获取音频和视频。
1 | navigator.mediaDevices |
视频方面,也可以准确定义视频画面的宽高:
1 | navigator.mediaDevices |
其他更多配置可参考:https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
3.2.2 video
(1) video 标签
1 | <video src="path/to/movie.mp4" controls="controls"> |
属性:
- autoplay: 如果出现该属性,则视频在就绪后马上播放
- controls:如果出现该属性,则向用户显示控件,比如播放按钮
- loop:如果出现该属性,则当媒介文件完成播放后再次开始播放
- muted:规定视频的音频输出应该被静音
- poster:规定视频下载时显示的图像,或者在用户点击播放按钮前显示的图像
- preload:如果出现该属性,则视频在页面加载时进行加载,并预备播放。如果使用 “autoplay”,则忽略该属性
- src:要播放的视频的 URL
- width:设置视频播放器的宽度,单位 px
- height:设置视频播放器的高度,单位 px
我们在进行音视频通话时,通常
本地视频(我方视频)应如下:
1 | <video id="local" muted autoplay>您的浏览器不支持 video 标签。</video> |
本地视频静音播放,因为我们无需我们自己发出的声音,因为我们到时候视频资源是从设备直接实时获取视频流,因此无需设置 src,并且设置autoplay
,可以让我们获取到视频流直接播放。
远程视频(对方视频)应如下:
1 | <video id="remote" poster="xxx" autoplay>您的浏览器不支持 video 标签。</video> |
远程视频同样设置autoplay
属性,让接收到的视频流直接播放。另外可设置一个poster
属性,可以在呼叫过程中或者被呼叫时,让页面显示呼叫中或者是显示对方头像肖像等,不然页面全黑会显得很尴尬。
(2) video 对象
使用音视频通话,我们控制音视频的播放基本通过js
实现的,就连前面介绍的video标签
一般都是通过js
创建。video
对象有很多属性,我这里只简单介绍部分属性,能基本满足 WebRTC 音视频通话。
我们要实现音视频实时通讯,传递的数据是音视频流,音视频流怎么让video
播放出来呢?看看下面代码:
1 | /** |
不难看出,video
对象有个srcObject
的属性,初始时该属性值是null
,将我们获取到音视频流直接赋值给该属性,我们的video
标签就可以实时播放了。上面这个例子是调用本地摄像头并展示到一个 id=local 的video
标签上,需要在 https 上就可以正常运行了。
我们如何关闭视频呢?
方法一:简单粗暴,关闭页面或者关闭浏览器。(你会让用户这么干么?)
方法二:使用MediaStream.getTracks()
,获取到所有媒体流轨道,每条轨道调用一个方法stop()
,就可以关闭当前流,摄像头也会停止录制。
1 | /** |
音频是第一条轨道,视频是第二条轨道,两个同时关闭即可。
3.3 WebRTC
官方文档(不推荐):https://www.w3.org/TR/webrtc/#peer-to-peer-connections
官方文档中文翻译(不推荐):https://github.com/RTC-Developer/WebRTC-Documentation-in-Chinese/tree/master/resource
MDN Web Docs(推荐):https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API
3.3.1 RTCPeerConnection
https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/
RTCPeerConnection
是浏览器之间点对点连接的核心 API,用于处理对等体之间流数据的稳定和有效通信,
1 | const pc = new RTCPeerConnection(serverConfig); |
serverConfig 包含 iceServers 参数,它包含有关 STUN 和 TURN 服务器相关信息数组,在查找 ICE 的时候候选使用。可以在网上找一些公共的 STUN 服务器,也可以使用前面章节我们自己通过 coturn 搭建的 STUN 服务器。
1 | const serverConfig = { |
(1) onicecandidate = eventHandler
作用:监听 RTCPeerConnection 实例上发生 icecandidate 事件,该函数会返回 ICE 协商结果,我们需要将结果发送给信令服务器,交由信令服务器转发给对方。
1 | pc.onicecandidate = (event) => { |
(2) ontrack = eventHandler
作用:监听 RTCPeerConnection 实例上接收到远程的数据流,该函数可获取到对端的媒体流。
1 | pc.ontrack = (event) => { |
(3) addTrack(track, stream…)
作用:设置轨道,该轨道将会在连同后传输到对端。
1 | async openCall(pc) { |
MDN 不建议使用 addStream()
(3) removeTrack(sender)
作用:删除轨道,删除已添加的轨道,用于挂断的时候
1 | var pc, sender; |
不建议的:onremovestream
(5) setLocalDescription()/setRemoteDescription()
setLocalDescription(sessionDescription):
设置本地 offer,将自己的描述信息加入到PeerConnection
中,参数类型:RTCSessionDescription
(见下一小节 3.2.2 RTCSessionDescription)
setRemoteDescription(sessionDescription):
设置远端的answer
,将对方的描述信息加入到PeerConnection
中,参数类型:RTCSessionDescription
(见下一小节 3.2.2 RTCSessionDescription)
通俗说:Alice
为了和Bob
建立合作关系(连接),Alice
我把拟好了一份合同,并签字了,我这里先保留扫描版,纸质合同通过快递(SDP)给你了,你通过快递(SDP)拿到合同后,先签字确认,这时候纸质合同上都有我们双方的签名了,但我这边还没有你的签名。你保存一下扫描版,然后通过快递把纸质再给我发回来,我拿到快递后,我也保存一下扫描版。这样,你我双放都有双方签名的扫描版合同。合同开始生效!
(6) createOffer()/createAnswer()
创建一个offer
,表示我方的请求。通常在 WebRTC 通信中,我们会请求对方接收我们的音频
和视频
数据。
1 | const offerOptions = { |
创建一个answer
,回应对方offer
。answer
也是有offer
作用的,在回应的时候,表示答应
你,并向你请求
。
打个比方:A 向 B 表白,请求 B 做 A 的女朋友。如果 B 接受了,表示 B 成了 A 女朋友。同时,这也有另外一层含义,表示 B 有请求:请 A 做我的男朋友。
1 | const answerOptions = { |
3.3.2 RTCSessionDescription
用于生成 Offer/Answer 协商过程中 SDP 协议的相关描述。
1 | new RTCSessionDescription(rtcDescription); |
rtcDescription 只有两个属性:type
,sdp
type
只能设置:’answer’,’offer’,’pranswer’,’rollback’;sdp
是标准的 SDP 会话描述(可由 createOffer/createAnswer 生成)
3.3.3 RTCIceCandidate
https://developer.mozilla.org/en-US/docs/Web/API/RTCIceCandidate
用于建立 ICE 连接。通常我们不会手动去实例化一个RTCIceCandidate
对象,在前面3.3.1 RTCPeerConnection
中的onicecandidate
事件回调就是一个RTCIceCandidate
对象,我们只需要了解其中几个属性即可。
- candidate: 用于连接性检测的对象
- sdpMid: candidate 的媒体流的识别标签
- sdpMLineIndex: candidate 的媒体流的相关联的 SDP 描述索引号
- address: 本机 IP 地址
- relatedAddress: 中继 IP
- port: 本机端口
- relatedPort: 中继端口
- component: 候选协议,只有两种情况:
RTP
(Real-Time Transport Protocol),RTCP
(Real-Time Transport Control Protocol) - foundation: 来自于 STUN 服务器的唯一标识符
- priority: 优先级
- tcpType: 如果使用的 TCP 协议,这个属性及表示 TCP 的状态
- type: RTCIceCandidateType 类型
- usernameFragment:
ice-ufrag
片段,用于生成ice-pwd
,同一 ICE 进程的连接都将使用的是同一个片段。
四、实战开发
前面基本上已经列举了大部分基础知识,现在开始运用起来。
本章实战开发,是开发一个 web 实时音视频聊天室 :输入相同房间号,即可加入聊天室,进行视频聊天。
主要有两个项目,前端界面(页面+WebRTC+socket.io),后端信令服务器控制转发(Express+socket.io)。
整个项目完整代码:http://223.223.179.203:8929/product/cp6666/webrtc-demo
4.1 环境准备
- anywhere:
npm i -g anywhere
4.2 信令服务器
因为信令服务器代码结构比较简单,咱们先开发信令服务器。观察1.5 经典WebRTC连接建立流程
,不难发现,信令服务器主要需要实现:转发offer
、转发answer
、转发candidate
的三大核心功能。此外,我们开发聊天室,还需要:创建聊天室、退出聊天室的功能。
4.2.1 搭建项目
(1)创建一个文件夹signal-server
,在目录下创建两个文件:
package.json
1 | { |
app.js
1 | const https = require("https"); // https服务 |
(2)创建证书
在项目文件夹下,创建一个文件夹keys
,然后开始生成自签名证书:
linux 环境下:
1 | openssl req -x509 -newkey rsa:2048 -keyout ./keys/server_key.pem -out ./keys/server_crt.pem -days 99999 -nodes |
windows 下:参考 https://letsencrypt.org/zh-cn/docs/certificates-for-localhost/
修改app.js
,将秘钥和签名证书的路径改为你电脑中的绝对路径,例如:
1 | //读取密钥和签名证书 |
(3)运行
在项目根目录下,安装依赖:
1 | npm i |
然后,启动:
1 | node app.js |
打开浏览器,访问:https://localhost:8443
访问时,浏览器会提示不安全的访问,这个时候,直接敲键盘:thisisunsafe
即可继续访问。当看到浏览器地址栏继续一直在请求中,那么就表示项目成功运行。
4.2.2 房间功能
房间功能主要包括:创建/加入房间、退出房间。
业务处理,都放在连接成功后的回调函数里。
(1)创建房间
1 | // socket监听连接 |
(2)退出房间
在加入房间监听后面,继续添加:
1 | // 退出房间,转发exit消息至room其他客户端 [from,room] |
还有一种情况,当 socket 连接异常断开时,也需要退出房间:
1 | // socket关闭 |
4.2.3 转发功能
转发功能有:转发offer
、转发answer
、转发candidate
(1)转发 offer
1 | // 转发offer消息至room其他客户端 [from,to,room,sdp] |
(2)转发 answer
1 | // 转发answer消息至room其他客户端 [from,to,room,sdp] |
(3)转发 candidate
1 | // 转发candidate消息至room其他客户端 [from,to,room,candidate[sdpMid,sdpMLineIndex,sdp]] |
4.2.4 完整代码
http://223.223.179.203:8929/product/cp6666/webrtc-demo/-/tree/master/signal-server
4.3 前端
前端可以分为三大功能:音视频设备控制和音视频显示控制、Offer/Answer 沟通、ICE 连接。
4.3.1 搭建项目
(1)创建一个文件夹webrtc-client
,在目录下创建一个index.html
文件,创建一个目录`js
1 | |- webrtc-client/ |
(2)在js
目录下创建几个文件,并在从网上下载socket.io.js
和jquery.min.js
文件
1 | |- webrtc-client/ |
(3)代码
index.html
1 | <html> |
config.js
1 | // WebRTC配置文件 |
4.3.2 兼容预处理
因为部分web API
在不同浏览器有不同的名称或者属性,因此需要处理兼容,以下是兼容代码,预先定义一下。
编辑sdk.js
:
1 | // 兼容处理 |
4.3.3 音视频控制
音视频控制主要分打开关闭摄像头,视频流绑定到video
标签,其实这一节前面3.2 音视频相关API
已经学习过了,这里直接给出代码。
接着编辑sdk.js
1 | /** |
编辑main.js
:
1 | /** |
测试一下摄像头功能,因为开启摄像头需要使用 https 服务,因此在前端项目根目录打开控制台命令,运行:
1 | anywhere 5000 |
然后浏览器打开命令行提示里的端口号为5001
的那个 https 协议的地址,例如:https://192.168.1.4:5001/
这时候,可能也会提示您的连接不是私密连接,点击高级
,最下面继续前往
。
点击连接
按钮,允许访问摄像头,看摄像头是否正常打开,页面视频是否出现,然后点击断开
,看摄像头是否关闭、画面是否消失。
4.3.4 Offer/Answer 模型
从这节开始,就正式涉及到WebRTC
相关 API 了,下面先写几个全局变量,用于保存一些公用数据:
编辑sdk.js
1 | // socket连接 |
(1)加入房间
在开始 Offer/Answer 模型前,我们必须得至少有两个客户端才行。因此,我们先写一下,怎么控制房间。
咱们先整理一下思路,我们先让甲
创建一个房间,然后,这个房间里只有甲
一个人,无法进行 Offer/Answer。这时候乙
在进入房间时,可以获取一下房间的人数,如果房间有人,那么乙
就给房间里的每一个人发送Offer
请求。房间里的甲
监听到了刚进来乙
的 Offer 后,给乙
回复 Answer。这样就建立起了Offer/Answer
模型。
编辑sdk.js
1 | /** |
前面,可以算是把Offer
发出去了,可以回顾4.2.3 转发功能
,信令服务器收到Offer
后,会将其转发给房间里的每一个用户,然后,我们就需要写一个监听,当信令服务器转发过来Offer
后,我们应该进行Answer
:
继续编辑sdk.js
1 | /** |
现在,我们把Answer
信息回复出去了,通过信令服务器会转发指定的用户(刚刚发来 offer 的用户),然后我们还要添加一个监听Answer
的信息:
继续编辑sdk.js
1 | /** |
(2)获取 RTCPeerConnection、移除 RTCPeerConnection
接上一步骤,其中涉及到一个getWebRTCConnect
的方法,这节就写如何实现它,以及本地如何管理与他人的连接。
继续编辑sdk.js
1 | // 对RTCPeerConnection连接进行缓存 |
4.3.5 ICE 连接/接收音视频流
Offer/Answer 模型让两个客户端互相建立了签订了合同,建立了信任的合作伙伴关系,接下来可以开始进行交易了(传输音视频数据)。在交易前,我们要互相知道对方真实的交易地址和银行账号(允许主机直连的地址,详细可回顾 1.4ICE 协议),我给你发货,你给我打钱。
通常,在第一步乙
的Offer
发出后,乙客户端
就开始通过 ICE 获取自己的地址(通过 ICE 协议可以了解,这个地址可能是自己的 IP 地址),只要等甲方
同意(设置远程描述完成,这时候可能还未回复 Answer),甲方
就可以接收到乙客户端
的音视频流了。同理,甲方
回复的Answer
之后,只要乙客户端
同意,乙客户端
也就能收到甲方
的音视频流了。至此,双方都收到对方的视频流了,视频通话建立。
回顾上一小节 4.3.4 (2) 获取RTCPeerConnection
中的一段代码:
1 | // 构建RTCPeerConnection |
实例pc
实际就是window.RTCPeerConnection
对象,这个对象有几个回调方法在3.3.1
节已经讲过了。
(1)onicecandidate
当 ICE 协商完成后,我们将协商结果发送至信令服务器,让其转发给指定的客户端。
继续编辑sdk.js
1 | /** |
远程客户端收到 candidate 后,添加 candidate 后即可接收到本机的音视频流:
继续编辑sdk.js
,添加监听事件:
1 | /** |
(2)ontrack
当监听到对方传递过来时音视频流后,动态创建一个video
标签,显示接收到的音视频流数据。
继续编辑sdk.js
1 | /** |
(3)onremovestream
监听对方停止传输视频流的时候,我方进行相应处理:
继续编辑sdk.js
1 | /** |
(4)添加本地音视频流
当我方开启摄像头后,全局变量localStream
就不为null
,我们需要往对方塞过去我们的的音视频数据,通过addTrack
方法。这样,在对方同意(添加我方描述)后,就可以获取到我方的音视频数据了。
4.3.6 完善逻辑
前面的内容基本把整个逻辑讲完了,但是你现在启动项目运行,是不是还是只能看到自己,后面的步骤根本没有执行?
因为前面的我们只打开了摄像头,还没有对接后续操作。
现在编辑main.js
,修改一下之前的代码:
1 | /** |
编辑sdk.js
,添加logout()
方法,监听他人退出房间socket.on('exit')
:
1 | /** |
4.3.7 完整代码
http://223.223.179.203:8929/product/cp6666/webrtc-demo/-/tree/master/webrtc-client
五、总结
现在,我们已经基本入门 WebRTC 了。可能前 3 章的协议、服务器、API 的学习让我们感觉很枯燥,知识很杂乱。我想,大家通过第四章的实战开发,将之前的知识点串通起来,是不是有一点感觉了。其实前两章在现在看来,是可以不必着重学习的。没有这些协议和服务器的支持,不懂他们的连接原理,后面的学习应该会更加疑惑吧。
前面的实战开发,是一个很简单的 Web 端的例子,没有涉及到安卓、iOS 端如何进行 WebRTC 通信,如果需要继续深入学习,下一步可以往移动端 WebRTC 上学习,比如移动端打开摄像头都和 Web 不同。
如果暂时没有深入 WebRTC 的学习话,可以基于这个实战项目进行横向的扩展。这个实战项目虽然看起来很简单,但是你可以给它加出很多功能来,会看起来很高大尚!比如:
- 在线电话:咱们现在只是通过房间号进行连接,我们可以设置一个登陆页面,将用户的 id 作为房间号,每个用户登陆后直接创建一个房间。我们想要给某个用户打音视频电话的话,我们可以加入他的房间,对方也能检测到房间是否有人进来,这样对方可以做成收到来电了,对方接听后,我们就进行 WebRTC 连接,实现拨打电话的功能。
- 视频会议:我们开发好注册登录功能,创建会议就相当于创建一个房间,只不过这个房间号是由我们系统来自动分配,别人登录后,通过该房间号就可以加入,即可实现视频会议功能。当然还可以扩展分享屏幕、白板等功能。
本次WebRTC入门
学习到此结束了,非常感谢您耐心地看完本篇长文。若有描述不对的地方,欢迎指出!
对以下文章、项目和视频的作者们,表示非常感谢!感谢您们辛苦的成果!
参考文章、文献、规范、项目、视频:
WebRTC 协议介绍:https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Protocols
WebRTC 中文社区:https://webrtc.org.cn/
RTC 开发者社区:https://rtcdeveloper.com/
又拍云 WebRTC 实时通信服务实践:https://segmentfault.com/a/1190000010339671
P2P 通信原理:https://zhuanlan.zhihu.com/p/26796476
STUN 协议详细介绍:https://zhuanlan.zhihu.com/p/26797664
TURN 协议详细介绍:https://zhuanlan.zhihu.com/p/26797422
ICE 协议详细介绍:https://zhuanlan.zhihu.com/p/26857913
WebRTC PeerConnection 建立连接过程:https://aggresss.blog.csdn.net/article/details/106832965
STUN/TURN 服务器(C 语言):https://github.com/coturn/coturn
STUN 服务器(node)https://github.com/enobufs/stun
Build Zoom Clone Video Chat Web App in Node.js Express and Socket.io Using WebRTC and PeerJS Library:https://www.youtube.com/watch?v=MX_r3Wm_BLE
Build Video Chat Web App From Scratch in 40 mins:https://www.youtube.com/watch?v=KLCcCTFivhM
coturn 服务器搭建:https://www.jianshu.com/p/915eab39476d
coturn 服务器搭建:https://meetrix.io/blog/webrtc/coturn/installation.html
WebRtcRoomServer(信令服务器 node):https://github.com/qdgx/WebRtcRoomServer
MDN Web Docs:https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API
webRTC API 之 RTCPeerConnection:https://www.cnblogs.com/suRimn/p/11314914.html
RTP 与 RTCP 协议介绍:https://blog.51cto.com/zhangjunhd/25481