当前位置: 首页 > news >正文

【前端实时音视频系列】WebRTC入门概览

在前端领域,WebRTC是一个相对小众的技术;但对于在线教育而言,却又是非常的核心。网上关于WebRTC的文章很多,本文将尝试以WebRTC工作过程为脉络进行介绍,让读者对这门技术有一个完整的概念。

WebRTC(Web Real-Time Communications) 是由谷歌开源并推进纳入W3C标准的一项音视频技术,旨在通过点对点的方式,在不借助中间媒介的情况下,实现浏览器之间的实时音视频通信。

与Web世界经典的B/S架构最大的不同是,WebRTC的通信不经过服务器,而直接与客户端连接,在节省服务器资源的同时,提高通信效率。为了做到这点,一个典型的WebRTC通信过程,包含四个步骤:找到对方,进行协商,建立连接,开始通讯。下面将分别阐述这四个步骤。

01

找到对方

虽然不需要经过服务器进行通信,但是在开始通信之前,必须知道对方的存在,这个时候就需要信令服务器

信令服务器

所谓信令(signaling)服务器,是一个帮助双方建立连接的「中间人」,WebRTC并没有规定信令服务器的标准,意味着开发者可以用任何技术来实现,如WebSocket或AJAX。

发起WebRTC通信的两端被称为对等端(Peer),成功建立的连接被称为PeerConnection,一次WebRTC通信可包含多个PeerConnection

const pc2 = new RTCPeerConnection({...});

在寻找对等端阶段,信令服务器的工作一般是标识与验证参与者的身份,浏览器连接信令服务器并发送会话必须信息,如房间号、账号信息等,由信令服务器找到可以通信的对等端并开始尝试通信。

其实在整个WebRTC通信过程中,信令服务器都是一个非常重要的角色,除了上述作用,SDP交换、ICE连接等都离不开信令,后文将会提到。

02

进行协商

协商过程主要指SDP交换

SDP协议

SDP(Session Description Protocol)指会话描述协议,是一种通用的协议,使用范围不仅限于WebRTC。主要用来描述多媒体会话,用途包括会话声明、会话邀请、会话初始化等。

在WebRTC中,SDP主要用来描述:

  • 设备支持的媒体能力,包括编解码器等

  • ICE候选地址

  • 流媒体传输协议

SDP协议基于文本,格式非常简单,它由多个行组成,每一行都为一下格式:

type=value

其中,type表示属性名,value表示属性值,具体格式与type有关。下面是一份典型的SDP协议样例:

v=0
o=alice 2890844526 2890844526 IN IP4 host.anywhere.com
s=
c=IN IP4 host.anywhere.com
t=0 0
m=audio 49170 RTP/AVP 0
a=rtpmap:0 PCMU/8000
m=video 51372 RTP/AVP 31
a=rtpmap:31 H261/90000
m=video 53000 RTP/AVP 32
a=rtpmap:32 MPV/90000

其中:

  1. v=代表协议版本号

  2. o=代表会话发起者,包括usernamesessionId

  3. s=代表session名称,为唯一字段

  4. c=代表连接信息,包括网络类型、地址类型、地址等

  5. t=代表会话时间,包括开始/结束时间,均为0表示持久会话

  6. m=代表媒体描述,包括媒体类型、端口、传输协议、媒体格式等

  7. a=代表附加属性,此处用于对媒体协议进行扩展

Plan B VS Unified Plan

在WebRTC发展过程中,SDP的语义(semantics)也发生了多次改变,目前使用最多的是Plan BUnified Plan两种。两者均可在一个PeerConnection中表示多路媒体流,区别在于:

  • Plan B:所有视频流和所有音频流各自放在一个m=值里,用ssrc区分

  • Unified Plan:每路流各自用一个m=

目前最新发布的 WebRTC 1.0 采用的是Unified Plan,已被主流浏览器支持并默认开启。Chrome浏览器支持通过以下API获取当前使用的semantics:

// Chrome
RTCPeerconnection.getConfiguration().sdpSemantics; // 'unified-plan' or 'plan b'

协商过程

协商过程并不复杂,如下图所示:

会话发起者通过createOffer创建一个offer,经过信令服务器发送到接收方,接收方调用createAnswer创建answer并返回给发送方,完成交换。

// 发送方,sendOffer/onReveiveAnswer为伪方法const pc1 = new 
RTCPeerConnection();const offer = await pc1.createOffer();
pc1.setLocalDescription(offer);
sendOffer(offer);
onReveiveAnswer((answer) => {
  pc1.setRemoteDescription(answer);
});
// 接收方,sendAnswer/onReveiveOffer为伪方法
const pc2 = new RTCPeerConnection();
onReveiveOffer((offer) => {
  pc2.setRemoteDescription(answer);
  const answer = await pc2.createAnswer();
  pc2.setLocalDescription(answer);
  sendAnswer(answer);
});

值得注意的是,随着通信过程中双方相关信息的变化,SDP交换可能会进行多次。

03

建立连接

现代互联网环境非常复杂,我们的设备通常隐藏在层层网关后面,因此,要建立直接的连接,还需要知道双方可用的连接地址,这个过程被称为NAT穿越,主要由ICE服务器完成,所以也称为ICE打洞

ICE

ICE(Interactive Connectivity Establishment)服务器是独立于通信双方外的第三方服务器,其主要作用,是获取设备的可用地址,供对等端进行连接,由STUN(Session Traversal Utilities for NAT)服务器来完成。每一个可用地址,都被称为一个ICE候选项(ICE Candidate),浏览器将从候选项中选出最合适的使用。其中,候选项的类型及优先级如下:

  1. 主机候选项:通过设备网卡获取,通常是内网地址,优先级最高

  2. 反射地址候选项:由ICE服务器获取,属于设备在外网的地址,获取过程比较复杂,可以简单理解为:浏览器向服务器发送多个检测请求,根据服务器的返回情况,来综合判断并获知自身在公网中的地址

  3. 中继候选项:由ICE中继服务器提供,前两者都行不通之后的兜底选择,优先级最低

新建PeerConnection时可指定ICE服务器地址,每次WebRTC找到一个可用的候选项,都会触发一次icecandidate事件,此时可调用addIceCandidate方法来将候选项添加到通信中:

const pc = new RTCPeerConnection({
  iceServers: [
    { "url": "stun:stun.l.google.com:19302" },
    { "url": "turn:user@turnserver.com", "credential": "pass" }
  ] // 配置ICE服务器}); pc.addEventListener('icecandidate', e => {
  pc.addIceCandidate(event.candidate);});

通过候选项建立的ICE连接,可以大致分为下图两种情况:

  1. 直接P2P的连接,为上述 1&2 两种候选项的情况;

  2. 通过TURN(Traversal Using Relays around NAT)中继服务器的连接,为上述第三种情况。

同样的,由于网络变动等原因,通信过程中的ICE打洞,同样可能发生多次。

04

进行通信

WebRTC选择了UDP作为底层传输协议。为什么不选择可靠性更强的TCP?原因主要有三个:

  1. UDP协议无连接,资源消耗小,速度快

  2. 传输过程中少量的数据损失影响不大

  3. TCP协议的超时重连机制会造成非常明显的延迟

而在UDP之上,WebRTC使用了再封装的RTPRTCP两个协议:

  • RTP(Realtime Transport Protocol):实时传输协议,主要用来传输对实时性要求比较高的数据,比如音视频数据

  • RTCP(RTP Trasport Control Protocol):RTP传输控制协议,顾名思义,主要用来监控数据传输的质量,并给予数据发送方反馈。

在实际通信过程中,两种协议的数据收发会同时进行。

关键API

下面将以一个demo的代码,来展示前端WebRTC中都用到了哪些API:

HTML

<!DOCTYPE html><html><head>

    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, user-scalable=yes, initial-scale=1, maximum-scale=1">
    <meta name="mobile-web-app-capable" content="yes">
    <meta id="theme-color" name="theme-color" content="#ffffff">
    <base target="_blank">
    <title>WebRTC</title>
    <link rel="stylesheet" href="main.css"/></head><body><div id="container">
    <video id="localVideo" playsinline autoplay muted></video>
    <video id="remoteVideo" playsinline autoplay></video>

    <div class="box">
        <button id="startButton">Start</button>
        <button id="callButton">Call</button>
    </div></div><script src="https://webrtc.github.io/adapter/adapter-latest.js"></script><script src="main.js" async></script></body></html>

JS

'use strict';const startButton = document.getElementById('startButton');const callButton = document.getElementById('callButton');callButton.disabled = true;startButton.addEventListener('click', start);callButton.addEventListener('click', call);const localVideo = document.getElementById('localVideo');const remoteVideo = document.getElementById('remoteVideo');let localStream;let pc1;let pc2;const offerOptions = {
  offerToReceiveAudio: 1,
  offerToReceiveVideo: 1};async function start() {
  /**
   * 获取本地媒体流
   */  startButton.disabled = true;  const stream = await  navigator.mediaDevices.getUserMedia({audio: true, video: true});
  localVideo.srcObject = stream;
  localStream = stream;  callButton.disabled = false;}function gotRemoteStream(e) {
  if (remoteVideo.srcObject !== e.streams[0]) {
    remoteVideo.srcObject = e.streams[0];
    console.log('pc2 received remote stream');
    setTimeout(() => {
      pc1.getStats(null).then(stats => console.log(stats));
    }, 2000)  }}function getName(pc) {
  return (pc === pc1) ? 'pc1' : 'pc2';}function getOtherPc(pc) {
  return (pc === pc1) ? pc2 : pc1;}async function call() {
  callButton.disabled = true;
  /**
   * 创建呼叫连接
   */
  pc1 = new RTCPeerConnection({
    sdpSemantics: 'unified-plan', // 指定使用 unified plan
    iceServers: [
        { "url": "stun:stun.l.google.com:19302" },
        { "url": "turn:user@turnserver.com", "credential": "pass" }
    ] // 配置ICE服务器
  }); 
  pc1.addEventListener('icecandidate', e => onIceCandidate(pc1, e)); // 监听ice候选项事件

  /**
   * 创建应答连接
   */
  pc2 = new RTCPeerConnection();

  pc2.addEventListener('icecandidate', e => onIceCandidate(pc2, e));
  pc2.addEventListener('track', gotRemoteStream);

  /**
   * 添加本地媒体流
   */
  localStream.getTracks().forEach(track => pc1.addTrack(track, localStream));

  /**
   * pc1 createOffer
   */
  const offer = await pc1.createOffer(offerOptions); // 创建offer
  await onCreateOfferSuccess(offer);}async function onCreateOfferSuccess(desc) {
  /**
   * pc1 设置本地sdp
   */
  await pc1.setLocalDescription(desc);

  /******* 以下以pc2为对方,来模拟收到offer的场景 *******/

  /**
   * pc2 设置远程sdp
   */
  await pc2.setRemoteDescription(desc);
    /**
   * pc2 createAnswer
   */
  const answer = await pc2.createAnswer(); // 创建answer
  await onCreateAnswerSuccess(answer);}async function onCreateAnswerSuccess(desc) {
  /**
   * pc2 设置本地sdp
   */
  await pc2.setLocalDescription(desc);

  /**
   * pc1 设置远程sdp
   */  await pc1.setRemoteDescription(desc);} async function onIceCandidate(pc, event) {
  try {
    await (getOtherPc(pc).addIceCandidate(event.candidate)); // 设置ice候选项
    onAddIceCandidateSuccess(pc);
  } catch (e) {
    onAddIceCandidateError(pc, e);
  }  console.log(`${getName(pc)} ICE candidate:\n${event.candidate ? event.candidate.candidate : '(null)'}`);}function onAddIceCandidateSuccess(pc) {
  console.log(`${getName(pc)} addIceCandidate success`);}function onAddIceCandidateError(pc, error) {
  console.log(`${getName(pc)} failed to add ICE Candidate: ${error.toString()}`);}

写在最后

作为「概览」,本文从比较浅的层次介绍了WebRTC技术,很多细节及原理性的内容,限于篇幅未作深入阐述。笔者也是刚接触几个月,如有谬误,还请告知。

推荐阅读

《WebRTC音视频开发:React+Flutter+Go实战》从基本概念、基础应用和综合案例系统介绍WebRTC技术的原理与应用,提供了前后端整体解决方案:PC-Web端使用的是React技术,后端使用的是Golang技术,移动端使用的是Flutter技术。

结合一对一视频通话案例,帮助读者快速上手,深入理解WebRTC的各种功能,并快速搭建自己的应用。

主要内容包括:WebRTC技术发展历史、应用场景、整体架构,WebRTC通话原理,Web开发环境搭建,HTML5项目简介,访问设备的设置,音视频设备的设置,音视频的录制,结合React+Flutter+Go技术开发音视频应用的案例。

更多精彩回顾

书讯 | 1月书讯:Hello 2021! (上)

书讯 | 1月书讯:Hello 2021! (下)

资讯 | TIOBE 1 月编程语言:Python 摘得 2020 年度编程语言!

书单 | 8本书助你了解人民日报“创作大脑”

干货 | 曾被“劝退”的 C++ 20 正式发布!

收藏 | DB-Engines:PostgreSQL获得“2020年度数据库”荣誉

上新 | ECharts开山之作,官方推荐!精心规划适合初学者的ECharts学习路径!

赠书 | 【第38期】移动边缘计算MEC,站在5G“中央”

点击阅读全文购买

相关文章:

  • 2021 年最值得学习的 5 大机器学习编程语言!
  • ECharts可视化经验分享
  • 2021年,AI哪个方向最火?看看大神们怎么说
  • 后“量子霸权”时代你不可错过的几本好书
  • Python 中最流行的十个标准库
  • IDC发布2021年中国人工智能市场10大预测
  • “绿宝书”好在哪?前端大佬们都在推荐
  • 【第39期】打破“打工人”魔咒,RPA 来狙击!
  • 适合的才是最好的,小众数据库黑马不可小觑
  • 刚刚拿下「中国AI最高奖」的语音技术,能给我们带来什么?
  • 百度官方文档Plus版,PaddlePaddle深度学习框架介绍
  • 华为首席开源联络官执笔,带你了解5G时代的边缘计算
  • 寒假到了,神兽归笼?程序员整治“熊孩子”有妙招
  • 读完《Effective Java》后,我总结了 50 条开发技巧
  • 手把手教你如何制作可视化大屏!
  • Android组件 - 收藏集 - 掘金
  • css属性的继承、初识值、计算值、当前值、应用值
  • Debian下无root权限使用Python访问Oracle
  • echarts的各种常用效果展示
  • electron原来这么简单----打包你的react、VUE桌面应用程序
  • ES6 ...操作符
  • flask接收请求并推入栈
  • GDB 调试 Mysql 实战(三)优先队列排序算法中的行记录长度统计是怎么来的(上)...
  • GitUp, 你不可错过的秀外慧中的git工具
  • js 实现textarea输入字数提示
  • Node 版本管理
  • thinkphp5.1 easywechat4 微信第三方开放平台
  • WePY 在小程序性能调优上做出的探究
  • 分类模型——Logistics Regression
  • 构建二叉树进行数值数组的去重及优化
  • 驱动程序原理
  • 如何优雅地使用 Sublime Text
  • Java总结 - String - 这篇请使劲喷我
  • RDS-Mysql 物理备份恢复到本地数据库上
  • ​一、什么是射频识别?二、射频识别系统组成及工作原理三、射频识别系统分类四、RFID与物联网​
  • # Apache SeaTunnel 究竟是什么?
  • ( 用例图)定义了系统的功能需求,它是从系统的外部看系统功能,并不描述系统内部对功能的具体实现
  • (多级缓存)多级缓存
  • (二)换源+apt-get基础配置+搜狗拼音
  • (附源码)ssm高校志愿者服务系统 毕业设计 011648
  • (附源码)计算机毕业设计SSM在线影视购票系统
  • (附源码)小程序儿童艺术培训机构教育管理小程序 毕业设计 201740
  • (论文阅读笔记)Network planning with deep reinforcement learning
  • (四)JPA - JQPL 实现增删改查
  • (一)Java算法:二分查找
  • (原創) 人會胖會瘦,都是自我要求的結果 (日記)
  • (转)Android中使用ormlite实现持久化(一)--HelloOrmLite
  • (转)nsfocus-绿盟科技笔试题目
  • (转载)在C#用WM_COPYDATA消息来实现两个进程之间传递数据
  • .NET Framework与.NET Framework SDK有什么不同?
  • .net Stream篇(六)
  • .NET 使用 JustAssembly 比较两个不同版本程序集的 API 变化
  • .NET/C# 反射的的性能数据,以及高性能开发建议(反射获取 Attribute 和反射调用方法)
  • .NET的数据绑定
  • .NET开发不可不知、不可不用的辅助类(一)