这篇文章主要记录自己最近研究音视频通信时候了解的知识,概念性以及注意事项的笔记,如果你目前对WebRTC几乎没什么了解,但是想比较快速的构建一个局域网/互联网的视频通信小功能,相较于详细的教程,那么这篇文章记录的内容主要在于梳理流程,可能对你有些帮助。
PS:屏幕发黄是因为使用了 f.lux,晚上的时候不那么刺眼
之前写Socket.io消息服务器即时通信的时候,就想可不可以用Socket.io来发送音视频流呢,这样就很简单的实现了视频通信的功能,搜索中了解到一个关键字“WebRTC”,然后打开了一扇大门...
WebRTC的介绍如下:
WebRTC is a free, open project that provides browsers and mobile applications with Real-Time Communications (RTC) capabilities via simple APIs. The WebRTC components have been optimized to best serve this purpose.
这项技术是谷歌/火狐等主流浏览器厂商一起发起的,意在通过方便的API,实现浏览器、移动平台,物联网设备点对点(peer-to-peer)的通信
需要服务器
音视频通话,即使是点对点的,但终究也需要一个服务器去交换信息,帮助用户A与用户B交换他们的地址,然后A与B才能进一步连接,不然A不知道B的地址,没办法发起请求连接。另外服务器也需要帮助它们进行打洞,不然A主动请求B,B所在网络的网关设备,百分之99.9会把请求直接丢弃掉。
EasyRTC 框架
服务器方面,因为我使用Node.js/Python,所以先在这两个生态环境里找解决方案,经过一些测试,选择了基于Node.js的EasyRTC框架(Github),足够的简单且例子丰富
它的文档记录了一些它的特点,在这里罗列一下
- 跨浏览器支持
- 开源免费
- 服务易部署
- 使用Websocket
- Pure Javascript
EasyRTC自带的Demo蛮丰富的,音频/视频通话,文件传输,视频录制,多人视频等,消息发送等。它集成了Socket.io,所以可以实时的传递消息,并且也有了房间的概念。直接使用WebRTC的API还是不那么方便,EasyRTC封装了常用的场景,还是挺方便的
你可以访问 https://rtc.yasking.org/demos/ 开查看这些Demo,这是我测试时候搭建的,用两台带有摄像头的电脑浏览器访问 https://rtc.yasking.org/demos/demo_audio_video_simple.html 就会看到上边图片效果
如果你打算测试一下EasyRTC框架,建议在虚拟机测试,运行很简单,参照文档,然后node server.js
即可
打开火狐浏览器访问服务器的8080端口,会看到Demo展示页面,使用Chrome访问不会加载成功,因为Chrome浏览器的安全策略,禁止非HTTPS网页访问本地摄像头等资源。如果必须使用Chrome,那么网页需要添加HTTPS支持,或者网页放在本地localhost:8000启动,访问的本地的网址,Chrome就不会限制了,Electron就可以这么做。
也许你使用互联网上的设备启动了EasyRTC服务,然后用电脑和手机4G都访问那个测试网页,会发现报错,有如下关键字,STUN/TURN/ICE,这是因为服务器没有设置“协助打洞”的服务,所以两个异网设备无法进行通信,想要异网通信,还得了解点知识才行,但是目前,如果在局域网部署的服务,那么局域网下的两个设备访问网址,视频通信是没有问题的,需要注意的是苹果手机,我测试的时候使用自带Safari浏览器,Chrome/火狐无法触发请求摄像头权限
先行知识
TCP打洞
因为网络IPv4资源紧张,所以网络中使用NAT技术很常见,比如一个路由器就组成了一个NAT网络,路由器外部是没办法主动去连接路由器内部网络设备的,假如你是A设备,想要主动连接在路由器后边的B设备,路由器接到你的访问请求时,几乎肯定会丢弃你的请求,不会进行请求转发,而WebRTC技术的目的是点对点通信,首要就需要解决这个问题,这时就需要一些技术,使得在NAT网络下,也能使得不同网络下的设备可以直接连接,进行通信,NAT好比一个墙,而穿过这个墙就称之为“打洞”
NAT的四种类型
- 全锥形
- 受限锥形
- 端口受限锥形
- 对称型
这样理解:当我的电脑A通过路由器S访问服务器B上网站时,路由器做了这样的事,假设电脑A的浏览器默认开启了10077端口来接受服务器返回的数据,那么路由器S将10077端口绑定到了它外部IP:803上,也就是说,服务器返回给数据给路由器的时候是发送给803端口的,路由器查看映射表得知,这个803端口,我分配给了电脑A:10077,就把数据发送给了A电脑的10077端口,由此可见,电脑A主动请求服务器B,路由器创建了一个绑定,通过这个绑定,外部的服务器可以发送数据给803,然后中转给电脑A,它们之间建立了连接,我们就可以称电脑A在路由器上打了个洞。
而这NAT四种类型,主要的区别的路由器建立了映射后,外部服务器发送数据给内部网络时的安全等级
- 全锥形:无限制,这个洞打开后,外部任意服务器IP及端口都可以发送数据到路由器IP:803端口,路由器直接转发数据给电脑A,这个安全级别很低,网络上这种类型的NAT几乎没有。
- 受限锥形:限制IP,电脑A访问过服务器B的IP后,那么服务器B就可以发送数据给路由器的803端口,也就是电脑A的监听端口10077,只要是服务器B的IP,就都可以通过
- 端口受限锥形:限制IP和端口,电脑A访问过服务器B的IP及端口,比如网站是80,那么服务器B只能通过80这个端口回复数据给电脑A,其它端口返回的数据会被路由器丢弃
- 对称型:这个和锥形不一样,锥形NAT是电脑A在路由器打洞后,电脑A监听的10077与路由器上的803端口绑定了,即使访问服务器C,D这个绑定关系依然不变,通过803就能访问A的10077,对称型不一样,电脑A访问一个新的服务器,路由器就为它绑定一个新的映射关系,和服务器B的80端口通信,路由器用的803端口,和B的8080端口通信,路由器用的804端口,和服务器C又用其它端口
以上就是四种NAT方式,理解后对WebRTC的点对点连接很有帮助
锥形NAT打洞流程
上边可知,电脑A访问服务器S,完成了路由器的打洞,洞是打完了,但是电脑A不知道路由器给自己分配的外部IP和端口,如果服务器S是一个特定的程序,S是知道电脑A经路由器映射出来的外网地址的,然后B连接后,S也知道B的外网地址,就可以通过一个流程让他们两个互联了,下边是锥形NAT打洞的流程
- Server开启两个侦听,一个叫【主连接】侦听,一个叫【协助打洞】监听
- A与B分别主动与S的主连接保持连接(比如Socket.io通信)
- A想和B建立TCP连接,A连接S的协助打洞,并在同端口启动侦听
- S通过主连接告知B客户端A的外网地址
- B收到S的通知后首先与S的协助打洞端口连接,随便发送一点数据后断开以便S知道B的外网地址
- B尝试与A经过NAT-A转换后的公网IP与端口进行连接,基本上连接请求会被NAT-A丢弃,但是NAT-B已经记录了A的地址,为接下来真正的连接做好准备,即B向A打了一个洞
- 客户端B打洞的同时在相同的端口启动侦听,之后向S主连接回复消息“我准备好了”,S在收到消息后经过NAT-B转换后的公网IP与端口告知A
- A收到S的主连接回复后,得知B的公网IP与端口后,开始发起主动连接到B的TCP连接,此时NAT-A记录了B的地址,即A向B打了一个洞,因为在[6]中,NAT-B记录过A的地址,所以SYN数据不会被丢弃,B的回复也会通过NAT-A的网络,这样,A与B的TCP连接就建立起来了
这样流程适用于锥形NAT,为什么对称型NAT无法打洞只能通过中继服务器转发呢?
因为B访问A时,NAT-B分配了新的端口(假设:803),并不会使用与S连接的(假设:801)端口,S不知道新开的803端口,在通知A的时候告诉B的端口是801,A访问B的时候,访问801,然而NAT-B为A开的端口号是803,会导直接被NAT-B丢弃,因为801是为S开的端口,A发往801肯定是不行的,所以对称NAT这种,没法进行NAT打洞,只能通过先发往服务器,服务器接收到流媒体后再通过801发送给B服务器,B服务器同理
参考:NAT路由器打洞原理
STUN / TURN / ICE
了解了NAT打洞的流程,及两个客户端是如何建立起连接的
这个协助打洞的服务器就是基于STUN和TURN协议的,STUN协议可以自动检测NAT类型,C/S架构,缺点是不支持对称NAT网络的打洞。TURN协议思路与STUN类似,不过它可以使用relay中继方式实现对称NAT的穿透,也就是当客户端类型是对称网络时,由服务器转发音视频流量,直连的洞虽然没打,但是音视频还是可以通过服务器来转发,实现通信,这也是为什么网络上免费的STUN服务器很多,TURN免费服务器却几乎没有的原因。
ICE是一个协商机制,程序中可以设置多个TURN和STUN服务器,它可以自动分析,选用最优的路线
coturn服务器
coturn是一个C语言写的stun/turn服务器,它支持两种协议
地址:https://github.com/coturn/coturn
我测试的时候,觉得使用Docker部署更加的方便,我使用的是这个
https://hub.docker.com/r/instrumentisto/coturn
listening-ip=172.27.0.3
listening-port=3478
relay-ip=172.27.0.3
external-ip=118.24.102.22
relay-threads=500
lt-cred-mech
pidfile="/var/run/turnserver.pid"
min-port=49152
max-port=65535
user=admin:123456
realm=Aha
新建一个my.conf文件,内容如上,字段解释网上很多,这里172.27.0.3是我的服务器内网IP,118.24.102.22是我的服务器外网IP,user指定了turn的用户名密码,这个可以单独写在一个文件里
docker run -d --network=host --name=coturn -v $(pwd)/my.conf:/etc/coturn/turnserver.conf instrumentisto/coturn --no-cli
启动Docker容器,可以先把-d换成--rf参数,看输出没问题了再后台运行也不迟
搭建好的coturn可以使用如下网址进行验证
验证服务器:
https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/
访问这个网站,删除原有的地址,填写自己部署的TURN或STUN服务器,生成链路
Time Component Type Foundation Protocol Address Port Priority
0.694 1 host 0 UDP 192.168.3.110 65049 126 | 32512 | 255
0.695 1 host 3 TCP 192.168.3.110 9 125 | 32704 | 255
0.696 2 host 0 UDP 192.168.3.110 65050 126 | 32512 | 254
0.696 2 host 3 TCP 192.168.3.110 9 125 | 32704 | 254
0.935 1 srflx 1 UDP 111.42.37.108 22759 100 | 32543 | 255
0.937 1 relay 2 UDP 118.21.102.22 65118 5 | 32543 | 255
0.949 2 srflx 1 UDP 111.42.37.108 22760 100 | 32543 | 254
0.950 2 relay 2 UDP 118.21.102.22 50684 5 | 32543 | 254
0.951 Done
有srflx等字眼,说明服务器部署没问题,通过服务器,我们获取到了自己在“外网”的地址
测试的时候,删除多余服务器,不然只要有一个好使的服务器就会干扰到想要测试的服务器,即使它没能正常工作,也能正常解析出公网地址
参考:
EasyRTC 设置 ICE
// easyrtc 框架设置ice服务器
var myIceServers = [
{"url":"stun:118.21.102.22:3478"},
{
"url":"turn:118.21.102.22:3478",
"username":"name",
"credential":"123456"
}
];
easyrtc.setOption("appIceServers", myIceServers);
服务器上部署
如果部署在外网,是需要配置HTTPS的,好在有免费的Let's Encrypt
HTTPS
申请Let's Encrypt的自动化脚本很多,此处不再详细记录
使用EasyRTC的server_ssl.js,引入证书
var webServer = https.createServer({
key: fs.readFileSync("/etc/letsencrypt/live/rtc.yasking.org/privkey.pem"),
cert: fs.readFileSync("/etc/letsencrypt/live/rtc.yasking.org/fullchain.pem")
}, httpApp);
最好是server_ssl.js监听本地127.0.0.1,然后使用Nginx做转发
server {
listen 443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;
server_name _;
ssl_certificate "/etc/letsencrypt/live/rtc.yasking.org/fullchain.pem";
ssl_certificate_key "/etc/letsencrypt/live/rtc.yasking.org/privkey.pem";
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 10m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# Load configuration files for the default server block.
include /etc/nginx/default.d/*.conf;
location / {
proxy_pass https://127.0.0.1:8443;
}
error_page 404 /404.html;
location = /40x.html {
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
}
}
将域名指向A记录的服务器IP,支持HTTPS访问的DEMO就不怕Chrome提示的安全问题无法调用本地摄像头了
Electron下使用
在Electron下使用EasyRTC无需HTTPS,这是因为它加载本地网页,内置的浏览器不会有安全问题
可以把EasyRTC的Demo目录拿出来,放在Electron本地加载使用。
需要修改的地方有:
1,jQuery引用
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
<script type="text/javascript" src="js/prettify/jquery.min.js"></script>
<script>if (window.module) module = window.module;</script>
提示找不到jQuery,在引入的地方前后加两行代码即可。
2,远程服务器IP
例如:demo_audio_vide_simple.js 是那个基础的页面引入的代码
function connect() {
easyrtc.setSocketUrl("http://192.168.38.142:8080");
easyrtc.setVideoDims(640,480);
easyrtc.setRoomOccupantListener(convertListToButtons);
easyrtc.easyApp("easyrtc.audioVideoSimple", "selfVideo", ["callerVideo"], loginSuccess, loginFailure);
}
在connect()函数里指定上EasyRTC的服务器即可。这样,本地网页访问摄像头没有权限问题,通信/协助打洞服务器无需部署HTTPS,方便了在局域网内使用,无需解决HTTPS自签证书的问题
3,资源相对路径
另外就是打开调试控制台(CTRL+SHIFT+I),看有没有什么资源,依赖的相对路径问题。
写在最后
目前了解到的就是这些,使用EasyRTC完成轻量化的视频通信,文件传输,视频录制没什么问题
但是有一个值得注意的事情:
因为连接都是点对点的,多人会议,如果你跟三个人通信,那么你的电脑就需要上传三份你的视频流量,如果人数很多,局域网还好,互联网的话很可能上行就满了,基础画质(640x480)大概220KB/s
所以针对一些简单场景,这个方案还是很不错的,从通用且实用来看,需要做的还有很多,比如媒体服务器
如果想要将设备的视频发布到媒体服务器,然后其它用户从媒体服务器获取,就可以实现一对多的直播了,这不是webrtc的针对场景,不过可能是一个需求,EasyRTC暂未集成媒体服务器方案,文档里说也许后续会有。
万丈高楼平地起,这个框架代码不多,研究一下,对了解WebRTC的流程还是有帮助的,另外在它的基础上二次开发也容易一些。
PS:这两天查询媒体服务器了解到一个开源的程序:Kurento,可以接入webrtc的源,除了传播、处理、加载和记录等常规媒体服务器的功能,也有自己的特色,比如语音识别、情感分析、人脸识别等,文档中提及了一个叫做openVidu的项目,也是Kurento团队发起的,和EasyRTC一样,都是为了简化用户开发,它相较于EasyRTC的优势在于它构建于Kurento之上,自带媒体服务器,功能会更加强大,同样,功能多了,学习起来的成本也会增加,先把EasyRTC用起来,熟悉了WebRTC,也清晰自己的使用场景及需求后,再来看openVidu,那可能会更快的消化吸收,直接啃Kurento+openVidu的文档,估计会很枯燥