本文由微医云技术团队前端工程师张宇航分享,原题“从0到1打造一个 WebRTC 应用”,有修订和改动。
去年初,突如其来的新冠肺炎疫情让线下就医渠道几乎被切断,在此背景下,在线问诊模式快速解决了大量急需就医病患的燃眉之急。而作为在线问诊中重要的一环——医患之间的视频问诊正是应用了实时音视频技术才得以实现。
众所周之,实时音视频聊天技术门槛很高,一般的公司要想在短时间内从零补齐这方面的技术短板相当困难,而开源音视频工程WebRTC提供了这样一个捷径(包括笔者公司的产品在内,同样是基于WebRTC技术才得以达成)。
本文将基于笔者公司开发的在线问诊产品中WebRTC技术的实践经验,讲述的如何基于WebRTC从零开发一个实时音视频聊天功能。文章会从WebRTC的基本知识、技术原理开始,基于开源技术为你演示如何搭建一个WebRTC实时音视频聊天功能。
学习交流:
- 即时通讯/推送技术开发交流5群:215477170 [推荐]
- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM》
- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK
(本文同步发布于:http://www.52im.net/thread-3680-1-1.html)
完整源码附件下载:
https://gitee.com/instant_messaging_network/learn-webrtc
cdwebrtc-server
yarn
npm start
cdwebrtc-static
yarn
npm start
在了解WebRTC技术之前,如果你对音视频技术的基础理论还不了解的话,建议优先从以下几篇入门文章先学一学。
▲ 图片引用自《了不起的WebRTC:生态日趋完善,或将实时音视频技术白菜化》
WebRTC(Web Real-Time Communication)是 Google 在 2010 年以 6820 万美元收购 VoIP 软件开发商 Global IP Solutions 的 GIPS 引擎,并改名为“WebRTC”于 2011 年将其开源的旨在建立一个互联网浏览器之间的音视频和数据实时通信的平台。更多WebRTC介绍详见《了不起的WebRTC:生态日趋完善,或将实时音视频技术白菜化》,本文不做赘述。
那么 WebRTC 能做些什么呢?
除了我们大家每天都在用的微信、钉钉、qq这类传统的IM社交软件中的实时音视频通话以外,笔者公司产品中涉及医疗领域中的在线问诊/远程门诊/远程会诊,还有时下较为流行的互动直播、在线教育等场景。除此之外,伴随着 5G 的快速建设,WebRTC 也为云游戏提供了很好的技术支撑。
WebRTC官方资源:
其它WebRTC学习资源:
来自WebRTC 官网的整体技术组成图:
整个WebRTC大致可以分为以下 3 部分:
因篇幅有限,本节不深入讨论,有兴趣可以读读《WebRTC实时音视频技术的整体架构介绍》。
P2P通信即点对点通信。
要实现两个不同网络环境(具有麦克风、摄像头设备)的客户端(可能是不同的 Web 浏览器或者手机 App)之间的实时音视频通信的难点在哪里、需要解决哪些问题?
总结一下,主要是下面这3个问题:
下面我们将逐个讨论这3个问题。
对于问题 1:WebRTC 虽然支持端对端通信,但是这并不意味着 WebRTC 不再需要服务器。
在P2P通信的过程中,双方需要交换一些元数据比如媒体信息、网络数据等等信息,我们通常称这一过程叫做“信令(signaling)”。
对应的服务器即“信令服务器 (signaling server)”,通常也有人将之称为“房间服务器”,因为它不仅可以交换彼此的媒体信息和网络信息,同样也可以管理房间信息。
比如:
为了避免出现冗余,并最大限度地提高与已有技术的兼容性,WebRTC 标准并没有规定信令方法和协议。在本文后面的实践章节会利用 Koa 和 Socket.io 技术实现一个信令服务器。
对于问题 2:我们首先要知道的是,不同浏览器对于音视频的编解码能力是不同的。
比如: Peer-A 端支持 H264、VP8 等多种编码格式,而 Peer-B 端支持 H264、VP9 等格式。为了保证双方都可以正确的编解码,最简单的办法即取它们所都支持格式的交集-H264。
在 WebRTC 中:有一个专门的协议,称为Session Description Protocol(SDP),可以用于描述上述这类信息。
因此:参与音视频通讯的双方想要了解对方支持的媒体格式,必须要交换 SDP 信息。而交换 SDP 的过程,通常称之为媒体协商。
对于问题 3:其本质上就是网络协商的过程,即参与音视频实时通信的双方要了解彼此的网络情况,这样才有可能找到一条相互通讯的链路。
理想的网络情况是每个浏览器的电脑都有自己的私有公网 IP 地址,这样的话就可以直接进行点对点连接。
但实际上:出于网络安全和 IPV4 地址不够的考虑,我们的电脑与电脑之间或大或小都是在某个局域网内,需要NAT(“Network Address Translation,” 中文译为“网络地址转换”)。在 WebRTC 中我们使用 ICE 机制建立网络连接。
那么何为 ICE?
ICE (Interactive Connecctivity Establishment, 交互式连接建立),ICE 不是一种协议,而是整合了 STUN 和 TURN 两种协议的框架。
其中:STUN(Sesssion Traversal Utilities for NAT, NAT 会话穿越应用程序),它允许位于 NAT(或多重 NAT)后的客户端找出自己对应的公网 IP 地址和端口,也就是俗称的P2P“打洞”。
但是:如果 NAT 类型是对称型的话,那么就无法打洞成功。这时候 TURN 就派上用场了,TURN(Traversal USing Replays around NAT)是 STUN/RFC5389 的一个拓展协议在其基础上添加了 Replay(中继)功能。
简单来说:其目的就是解决对称 NAT 无法穿越的问题,在 STUN 分配公网 IP 失败后,可以通过 TURN 服务器请求公网 IP 地址作为中继地址。
在 WebRTC 中有三种类型的 ICE 候选者,它们分别是:
从上图我们可以看出:在非本地局域网内 WebRTC 通过 STUN server 获得自己的外网 IP 和端口,然后通过信令服务器与远端的 WebRTC 交换网络信息,之后双方就可以尝试建立 P2P 连接了。当 NAT 穿越不成功时,则会通过 Relay server (TURN)中转。
值得一提的是:在 WebRTC 中网络信息通常用candidate来描述,而上述图中的 STUN server 和 Replay server 也都可以是同一个 server。在文末的实践章节即是采用了集成了 STUN(打洞)和 TURN(中继)功能的开源项目 coturn。
综上对三个问题的解释,我们可以用下图来说明 WebRTC 点对点通信的基本原理。
简而言之:就是通过 WebRTC 提供的 API 获取各端的媒体信息 SDP 以及 网络信息 candidate ,并通过信令服务器交换,进而建立了两端的连接通道完成实时视频语音通话。
PS:有关P2P的相关知识,可以深入学习一下文章:
音视频采集 API,即 MediaDevices.getUserMedia()。
示例代码:
const constraints = {
video: true,
audio: true
};
// 非安全模式(非https/localhost)下 navigator.mediaDevices 会返回 undefined
try{
const stream = await navigator.mediaDevices.getUserMedia(constraints);
document.querySelector('video').srcObject = stream;
} catch(error) {
console.error(error);
}
获取音视频设备输入输出列表API,即 MediaDevices.enumerateDevices()。
示例代码:
try{
const devices = await navigator.mediaDevices.enumerateDevices();
this.videoinputs = devices.filter(device => device.kind === 'videoinput');
this.audiooutputs = devices.filter(device => device.kind === 'audiooutput');
this.audioinputs = devices.filter(device => device.kind === 'audioinput');
} catch(error) {
console.error(error);
}
RTCPeerConnection 作为创建点对点连接的 API,是我们实现音视频实时通信的关键。
在本文的实践章节中,主要运用到了以下方法。
媒体协商方法:
重要事件:
在上个章节的描述中可以知道 P2P 通信中最重要的一个环节就是交换媒体信息。
媒体协商原理:
从上图不难发现,整个媒体协商过程可以简化为三个步骤对应上述四个媒体协商方法。
具体是:
经过上述三个步骤,则完成了 P2P 通信过程中的媒体协商部分。
实际上:在呼叫端以及接收端调用 setLocalDesccription 同时也开始了收集各端自己的网络信息(candidate),然后各端通过监听事件 onicecandidate 收集到各自的 candidate 并通过信令服务器传送给对端,进而打通 P2P 通信的网络通道,并通过监听 onaddstream 事件拿到对方的视频流进而完成了整个视频通话过程。
提示:本节所涉及的完整源码,请从本文“2、本文源码”一节的附件下载。
注意:如果只是本地局域网测试则无需搭建 [url=%5Burl=https://github.com/coturn/%5Dcoturn[/url]]coturn[/url] 服务器,如果需要外网访问在搭建 coturn 服务器之前你需要购买一台云主机以及绑定支持 https 访问的域名。以下是笔者自己搭建的过程,感兴趣的可以参照着自已实践一次。
coturn 服务器的搭建主要是为了解决 NAT 无法穿越的问题。
其安装也较为简单:
1. git clone [url=https://github.com/coturn/coturn.git]https://github.com/coturn/coturn.git[/url]
2. cdcoturn/
3. ./configure--prefix=/usr/local/coturn
4. make-j 4
5. makeinstall
//生成 key
6. openssl req -x509 -newkey rsa:2048 -keyout /etc/turn_server_pkey.pem -out /etc/turn_server_cert.pem -days 99999 -nodes
我的配置如下:
vim /usr/local/coturn/etc/turnserver.conf
listening-port=3478
external-ip=xxx.xxx //你的主机公网 IP
user=xxx:xxx //账号: 密码
realm=xxx.com //你的域名
我的启动过程:
1. cd/usr/local/coturn/bin/
2. ./turnserver-c ../etc/turnserver.conf
//注意:云主机内的 TCP 和 UDP 的 3478 端口都要开启
在编写代码之前,结合上述章节 WebRTC 点对点通信的基本原理,可以得出以下流程图。
从图中不难看出,假设 PeerA 为发起方,PeerB 为接收方要实现 WebRTC 点对点的实时音视频通信,信令(Signal)服务器是必要的,以管理房间信息以及转发网络信息和媒体信息的。
在本文中是利用 koa 及 socket.io 搭建的信令服务器:
// server 端 server.js
const Koa = require('koa');
const socket = require('socket.io');
const http = require('http');
const app = newKoa();
const httpServer = http.createServer(app.callback()).listen(3000, ()=>{});
socket(httpServer).on('connection', (sock)=>{
// ....
});
// client 端 socket.js
import io from 'socket.io-client';
const socket = io.connect(window.location.origin);
export defaultsocket;
在搭建好信令服务器后,结合流程图,有以下步骤。
步骤1:PeerA 和 PeerB 端分别连接信令服务器,信令服务器记录房间信息:
// server 端 server.js
socket(httpServer).on('connection', (sock)=>{
// 用户离开房间
sock.on('userLeave',()=>{
// ...
});
// 检查房间是否可加入
sock.on('checkRoom',()=>{
// ...
});
// ....
});
// client 端 Room.vue
import socket from '../utils/socket.js';
// 服务端告知用户是否可加入房间
socket.on('checkRoomSuccess',()=>{
// ...
});
// 服务端告知用户成功加入房间
socket.on('joinRoomSuccess',()=>{
// ...
});
//....
步骤2:A 端作为发起方向接收方 B 端发起视频邀请,在得到 B 同意视频请求后,双方都会创建本地的 RTCPeerConnection,添加本地视频流,其中发送方会创建 offer 设置本地 sdp 信息描述,并通过信令服务器将自己的 SDP 信息发送给对端
socket.on('answerVideo', async (user) => {
VIDEO_VIEW.showInvideoModal();
// 创建本地视频流信息
const localStream = await this.createLocalVideoStream();
this.localStream = localStream;
document.querySelector('#echat-local').srcObject = this.localStream;
this.peer = newRTCPeerConnection();
this.initPeerListen();
this.peer.addStream(this.localStream);
if(user.sockId === this.sockId) {
// 接收方
} else{
// 发送方 创建 offer
const offer = await this.peer.createOffer(this.offerOption);
await this.peer.setLocalDescription(offer);
socket.emit('receiveOffer', { user: this.user, offer });
}
});
步骤3:前面提起过其实在调用 setLocalDescription 的同时,也会开始收集自己端的网络信息(candidate),如果在非局域网内或者网络“打洞”不成功,还会尝试向 Stun/Turn 服务器发起请求,也就是收集“中继候选者”,因此在创建 RTCPeerConnection 我们还需要监听 ICE 网络候选者的事件:
init PeerListen () {
// 收集自己的网络信息并发送给对端
this.peer.onicecandidate = (event) => {
if(event.candidate) { socket.emit('addIceCandidate', { candidate: event.candidate, user: this.user }); }
};
// ....
}
步骤4:当接收方 B 端通过信令服务器拿到对端发送方 A 端的含有 SDP 的 offer 信息后则会调用 setRemoteDescription 存储对端的 SDP 信息,创建及设置本地的 SDP 信息,并通过信令服务器传送含有本地 SDP 信息的 answer:
socket.on('receiveOffer', async (offer) => {
await this.peer.setRemoteDescription(offer);
const answer = await this.peer.createAnswer();
await this.peer.setLocalDescription(answer);
socket.emit('receiveAnsewer', { answer, user: this.user });
});
步骤5:当发起方 A 通过信令服务器接收到接收方 B 的 answer 信息后则也会调用 setRemoteDescription,这样双方就完成了 SDP 信息的交换:
socket.on('receiveAnsewer', (answer) => {
this.peer.setRemoteDescription(answer);
});
步骤6:当双方 SDP 信息交换完成并且监听 icecandidate 收集到网络候选者通过信令服务器交换后,则会拿到彼此的视频流:
socket.on('addIceCandidate', async (candidate) => {
await this.peer.addIceCandidate(candidate);
});
this.peer.onaddstream = (event) => {
// 拿到对方的视频流
document.querySelector('#remote-video').srcObject = event.stream;
};
经过上个章节的6个步骤,即可完成一个基于WebRTC的完整 P2P 视频实时通话功能(代码可通过:本节所涉及的完整源码,请从本文“2、本文源码”一节的附件下载)。
值得一提的是:代码中的 VIDEO_VIEW 是专注于视频UI层的JS SDK,包含了发起视频 Modal、接收视频 Modal、视频中 Modal,其是从笔者线上 Web 视频问诊产品所使用的 JS SDK 抽离出来的。
本文只是简单地介绍了WebRTC P2P的通信基本原理以及简单的代码实践,事实上我们生产环境所使用的 SDK 不仅支持点对点通信,还支持多人视频通话,屏幕共享等功能这些都是基于WebRTC实现的。
[1] WebRTC标准API在线文档
[2] WebRTC in the real world: STUN, TURN and signaling
[3] WebRTC 信令控制与 STUN/TURN 服务器搭建
[4] 了不起的WebRTC:生态日趋完善,或将实时音视频技术白菜化
[5] 开源实时音视频技术WebRTC在Windows下的简明编译教程
[6] WebRTC实时音视频技术的整体架构介绍
[7] 良心分享:WebRTC 零基础开发者教程(中文)[附件下载]
[8] P2P技术详解(二):P2P中的NAT穿越(打洞)方案详解(基本原理篇)
[9] P2P技术详解(四):P2P技术之STUN、TURN、ICE详解
[10] 通俗易懂:快速理解P2P技术中的NAT穿透原理
[1] 开源实时音视频技术WebRTC的文章:
《开源实时音视频技术WebRTC的现状》
《简述开源实时音视频技术WebRTC的优缺点》
《访谈WebRTC标准之父:WebRTC的过去、现在和未来》
《良心分享:WebRTC 零基础开发者教程(中文)[附件下载]》
《WebRTC实时音视频技术的整体架构介绍》
《新手入门:到底什么是WebRTC服务器,以及它是如何联接通话的?》
《WebRTC实时音视频技术基础:基本架构和协议栈》
《浅谈开发实时视频直播平台的技术要点》
《[观点] WebRTC应该选择H.264视频编码的四大理由》
《基于开源WebRTC开发实时音视频靠谱吗?第3方SDK有哪些?》
《开源实时音视频技术WebRTC中RTP/RTCP数据传输协议的应用》
《简述实时音视频聊天中端到端加密(E2EE)的工作原理》
《实时通信RTC技术栈之:视频编解码》
《开源实时音视频技术WebRTC在Windows下的简明编译教程》
《网页端实时音视频技术WebRTC:看起来很美,但离生产应用还有多少坑要填?》
《了不起的WebRTC:生态日趋完善,或将实时音视频技术白菜化》
《腾讯技术分享:微信小程序音视频与WebRTC互通的技术思路和实践》
《融云技术分享:基于WebRTC的实时音视频首帧显示时间优化实践》
《零基础入门:基于开源WebRTC,从0到1实现实时音视频聊天功能》
>> 更多同类文章 ……
[2] 实时音视频开发的其它精华资料:
《即时通讯音视频开发(一):视频编解码之理论概述》
《即时通讯音视频开发(二):视频编解码之数字视频介绍》
《即时通讯音视频开发(三):视频编解码之编码基础》
《即时通讯音视频开发(四):视频编解码之预测技术介绍》
《即时通讯音视频开发(五):认识主流视频编码技术H.264》
《即时通讯音视频开发(六):如何开始音频编解码技术的学习》
《即时通讯音视频开发(七):音频基础及编码原理入门》
《即时通讯音视频开发(八):常见的实时语音通讯编码标准》
《即时通讯音视频开发(九):实时语音通讯的回音及回音消除概述》
《即时通讯音视频开发(十):实时语音通讯的回音消除技术详解》
《即时通讯音视频开发(十一):实时语音通讯丢包补偿技术详解》
《即时通讯音视频开发(十二):多人实时音视频聊天架构探讨》
《即时通讯音视频开发(十三):实时视频编码H.264的特点与优势》
《即时通讯音视频开发(十四):实时音视频数据传输协议介绍》
《即时通讯音视频开发(十五):聊聊P2P与实时音视频的应用情况》
《即时通讯音视频开发(十六):移动端实时音视频开发的几个建议》
《即时通讯音视频开发(十七):视频编码H.264、VP8的前世今生》
《即时通讯音视频开发(十八):详解音频编解码的原理、演进和应用选型》
《即时通讯音视频开发(十九):零基础,史上最通俗视频编码技术入门》
《实时语音聊天中的音频处理与编码压缩技术简述》
《网易视频云技术分享:音频处理与压缩技术快速入门》
《学习RFC3550:RTP/RTCP实时传输协议基础知识》
《基于RTMP数据传输协议的实时流媒体技术研究(论文全文)》
《声网架构师谈实时音视频云的实现难点(视频采访)》
《浅谈开发实时视频直播平台的技术要点》
《还在靠“喂喂喂”测试实时语音通话质量?本文教你科学的评测方法!》
《实现延迟低于500毫秒的1080P实时音视频直播的实践分享》
《移动端实时视频直播技术实践:如何做到实时秒开、流畅不卡》
《如何用最简单的方法测试你的实时音视频方案》
《技术揭秘:支持百万级粉丝互动的Facebook实时视频直播》
《简述实时音视频聊天中端到端加密(E2EE)的工作原理》
《移动端实时音视频直播技术详解(一):开篇》
《移动端实时音视频直播技术详解(二):采集》
《移动端实时音视频直播技术详解(三):处理》
《移动端实时音视频直播技术详解(四):编码和封装》
《移动端实时音视频直播技术详解(五):推流和传输》
《移动端实时音视频直播技术详解(六):延迟优化》
《理论联系实际:实现一个简单地基于HTML5的实时视频直播》
《IM实时音视频聊天时的回声消除技术详解》
《浅谈实时音视频直播中直接影响用户体验的几项关键技术指标》
《如何优化传输机制来实现实时音视频的超低延迟?》
《首次披露:快手是如何做到百万观众同场看直播仍能秒开且不卡顿的?》
《Android直播入门实践:动手搭建一套简单的直播系统》
《网易云信实时视频直播在TCP数据传输层的一些优化思路》
《实时音视频聊天技术分享:面向不可靠网络的抗丢包编解码器》
《P2P技术如何将实时视频直播带宽降低75%?》
《专访微信视频技术负责人:微信实时视频聊天技术的演进》
《腾讯音视频实验室:使用AI黑科技实现超低码率的高清实时视频聊天》
《微信团队分享:微信每日亿次实时音视频聊天背后的技术解密》
《近期大热的实时直播答题系统的实现思路与技术难点分享》
《福利贴:最全实时音视频开发要用到的开源工程汇总》
《七牛云技术分享:使用QUIC协议实现实时视频直播0卡顿!》
《实时音视频聊天中超低延迟架构的思考与技术实践》
《理解实时音视频聊天中的延时问题一篇就够》
《实时视频直播客户端技术盘点:Native、HTML5、WebRTC、微信小程序》
《写给小白的实时音视频技术入门提纲》
《微信多媒体团队访谈:音视频开发的学习、微信的音视频技术和挑战等》
《腾讯技术分享:微信小程序音视频技术背后的故事》
《微信多媒体团队梁俊斌访谈:聊一聊我所了解的音视频技术》
《新浪微博技术分享:微博短视频服务的优化实践之路》
《实时音频的混音在视频直播应用中的技术原理和实践总结》
《以网游服务端的网络接入层设计为例,理解实时通信的技术挑战》
《腾讯技术分享:微信小程序音视频与WebRTC互通的技术思路和实践》
《新浪微博技术分享:微博实时直播答题的百万高并发架构实践》
《技术干货:实时视频直播首屏耗时400ms内的优化实践》
《爱奇艺技术分享:轻松诙谐,讲解视频编解码技术的过去、现在和将来》
《零基础入门:实时音视频技术基础知识全面盘点》
《实时音视频面视必备:快速掌握11个视频技术相关的基础概念》
《淘宝直播技术干货:高清、低延时的实时视频直播技术解密》
《实时音视频开发理论必备:如何省流量?视频高度压缩背后的预测技术》
>> 更多同类文章 ……
本文已同步发布于“即时通讯技术圈”公众号。
同步发布链接是:http://www.52im.net/thread-3680-1-1.html