前言

作为一个认为啥都想懂一点的小开发,一直都对WebRTC很感兴趣,这个兴趣来源于几年前公司希望做一个即时通讯的小功能在APP上,不过最终由于项目最终需求更改而搁置。虽然如此,但是我还是了解了一些关于该技术的技术背景,例如P2P通讯、内网打洞等等。通过几个晚上的学习和实验,大体上了解WebRTC的原理和使用方法,现在分享一下我的学习过程吧。

准备工作

作为一个文档党,从来都要先看官方文档和文章,这样才能保证自己拿到最新,最好的一手信息。WebRTC官网文档也还算是比较全面,不过貌似都好久没更新了。推测是,大概很久没有做功能升级了吧。我这次学习,参考了一些官方例子,加上了自己的理解。有错误的地方大家可以指出来呀,一起学习。参考的文章会在文章结尾加上。废话不多说了,开始吧。

打开我们的摄像头

WebRTC是谷歌开发的,目标是创造一个高质量的、可靠的通讯框架,从字面的意我们可以拆分为了WebRTC两部分,Web很好理解啊,就是基于网络,而RTC全称为Real Time Communications(实时通讯),因此它的作用就是让我们可以利用浏览器(也能用于APP),进行实时的通讯的一个框架。既然是通讯媒介当然是多种的,包括视频,语音,文本等多种多媒体信息,甚至你还能利用它来传输各种文件。下面,我们用最直观的,视频通讯来开始我们的学习吧。

用浏览器打开摄像头很简单,我们可以直接调用JS API 实现。

  • HTML
<!DOCTYPE html>
<html lang="en">
<head>...
</head>
<body><h1>获得视频流</h1><!-- 设置自动播放 --><video autoplay playsinline></video><script src="js/main.js"></script>
</body>
</html>
复制代码
  • JavaScript
// 媒体流配置
const mediaStreamConstraints = {video: true
};// 获得 video 标签元素
const localVideo = document.querySelector("video");// 媒体流对象
let localStream;// 回调保存视频流对象并把流传到 video 标签
function gotLocalMediaStream(mediaStream) {localStream = mediaStream;localVideo.srcObject = mediaStream;
}// handle 错误信息
function handleLocalMediaStreamError(error) {console.log("打开本地视频流错误: ", error)
}// fire!!
navigator.mediaDevices.getUserMedia(mediaStreamConstraints).then(gotLocalMediaStream).catch(handleLocalMediaStreamError);
复制代码

代码主要分2步

  1. navigator.mediaDevices.getUserMedia 中获得视频设备。
  2. then 的回调中把视频流传到 video 标签。

非常简单吧

值得注意的是,我用的是Chrome 浏览器,新版本的Chrome加强了获取设备的安全策略。如果你想要打开摄像头等设备,你的域名如果不是本地文件或者 localhost 那必须通过https 访问。

使用 RTC 进行 P2P 传输

既然视频流我们得到了,第二步,我们来使用WebRTCRTCPeerConnection 来进行本地传输吧。这个Demo 不是真实的使用场景,因为不涉及到真实世界的网络传输,我们仅仅是在同一个页面,打开了两个 RTCPeerConnection 把一个的内容传输到另一个,从而进行通讯。在贴代码之前,我们先来简单的描述一下创建连接的过程吧。

假设现在是A想跟B视频。他们的 offer/answer (申请?/ 应答?), 机制是这样的:

1. `A `创建了一个 `RTCPeerConnection` 对象2. `A` 利用`RTCPeerConnection` 的 `createOffer()` 方法创建了一个 `offer` (一个` SDP` 的会话描述)3. `A` 在 `offer` 的回调中使用 `setLocalDescription()` 方法存储他的 `offer` 4. `A` 把他的 `offer` 字符串化,然后通过某一种信令机制发给 `B`5. `B` 收到 `A` 的 `offer` 后用`setRemoteDescription()` 存起来,如此一来他的 `RTCPeerConnection` 就知道了 `A` 的配置。6. `B` 调用 `createAnswer()` 并用他的成功回调的传送他的本地会话描述:这就是 `B` 的`answer`7. `B` 用 `setLocalDescription()` 设置了他的 `answer` 到本地的会话描述8. 然后 `B` 用某一种信令机制把他的 `answer` 字符串化之后返回给 `A`9. `A` 把 `B` 的 `answer` 利用`setRemoteDescription()`方法存取为远程会话描述
复制代码

过程看上去很麻烦,不过其实他们就做了个事情

  1. 创建会话描述(SDP
  2. 交换会话描述(SDP
  3. 存储自己跟对方的会话描述

有关 SDP的格式,可以参看文章后面的链接

下面让我们看代码,走起

  • HTML
<!DOCTYPE html>
<html lang="en">
<head>...
</head>
<body><h1>RTCPeerConnection 传输视频流</h1><!-- 设置自动播放 --><video autoplay playsinline id="localVideo"></video><video autoplay playsinline id="remoteVideo"></video><div><button id="startBtn">开始</button><button id="callBtn">拨打</button><button id="hangupBtn">挂机</button></div><!-- 垫片,用于统一浏览器 API --><script src="js/adapter.js"></script><script src="js/main.js"></script>
</body>
</html>
复制代码

HTML 代码比较简单,我们创建了两个 video,一个显示远程一个显示本地,并且加入了三个按钮进行模拟拨打。细心的同学可能已经发现了,我们引入了一个垫片adapter.js。经常写前端的同学对垫片可能熟悉不过了,因为世界上不仅仅只有谷歌的浏览器,还有各种各样别的。然后命名,API也是各种各样,所以我们会利用各种垫片,统一我们的API。不再忍受兼容之苦。adapter.js就是这样的存在。他是谷歌官方提供给我们的。引入它我们便可以用统一套API操作。

  • JavaScript

由于代码比较长,就只贴关键代码了。全部代码链接我会在文章后面贴上。

// 开始按钮,打开本地媒体流
function startAction() {startButton.disabled = true;navigator.mediaDevices.getUserMedia(mediaStreamConstraints).then(gotLocalMediaStream).catch(handleLocalMediaStreamError);trace('本地媒体流打开中...');
}
复制代码

这是响应开始按钮的函数。跟第一个例子一样,主要是用来打开摄像头,并且把视频流传到idlocalVideo的视频标签。

// 拨打按钮, 创建 peer connection
function callAction() {callButton.disabled = true;hangupButton.disabled = false;trace("开始拨打...");startTime = window.performance.now();// ...const servers = null;  // RTC 服务器配置// 创建 peer connetcions 并添加事件localPeerConnection = new RTCPeerConnection(servers);trace("创建本地 peer connetcion 对象");localPeerConnection.addEventListener('icecandidate', handleConnection);localPeerConnection.addEventListener('iceconnectionstatechange', handleConnectionChange);remotePeerConnection = new RTCPeerConnection(servers);trace("创建远程 peer connetcion 对象");remotePeerConnection.addEventListener('icecandidate', handleConnection);remotePeerConnection.addEventListener('iceconnectionstatechange', handleConnectionChange);remotePeerConnection.addEventListener('addstream', gotRemoteMediaStream);// 添加本地流到连接中并创建连接localPeerConnection.addStream(localStream);trace("添加本地流到本地 PeerConnection");trace("开始创建本地 PeerConnection offer");localPeerConnection.createOffer(offerOptions).then(createdOffer).catch(setSessionDescriptionError);
}
复制代码

这部份是拨打按钮的响应函数。在这个方法中,我们做了个事情。

  1. 创建了用于通讯的一对RTCPeerConnection对象,localPeerConnectionremotePeerConnection

  2. 分别给两个RTCPeerConnection对象注册了icecandidate(重要)iceconnectionstatechange 事件的响应函数

  3. remotePeerConnection注册了addstream事件的响应。

  4. 把本地视频流添加到localPeerConnection

  5. localPeerConnection创建offer

这里有一个上面没有提及的东西ICE CandidateICE是啥呢?哈哈,他的全称是 Interactive Connectivity Establishment交互式连接的建立。他是一个规范,说白了就是建立连接用的规范,由于我们的WebRTC是要进行P2P连接的,而我们的网络是非常复杂的,而且大部分都是在内网(需要打洞或者穿越防火墙)。所以我们需要一个机制来建立内网连接。这个我会在后面的文章详细来说说。现在,简单理解成就是建立连接用的就好了。而icecandidate 的响应方法,则是当网络可用的情况下,用于存储和交换各种网络信息。

// 定义 RTC peer connection
function handleConnection(event) {const peerConnection = event.target;const iceCandidate = event.candidate;if (iceCandidate) {const newIceCanidate = new RTCIceCandidate(iceCandidate);const otherPeer = getOtherPeer(peerConnection);otherPeer.addIceCandidate(newIceCanidate).then(() => {handleConnectionSuccess(peerConnection);}).catch((error) => {handleConnectionFailure(peerConnection, error);});trace(`${getPeerName(peerConnection)} ICE candidate:\n` +`${event.candidate.candidate}.`);}
}
复制代码

这段代码正是体现了网络信息(ICE candidate),的保存和交换过程。而保存Candidate是通过调用RTCPeerConnection对象的addIceCandidate方法。这里可能大家有疑问,这里就交换了Candidate信息了吗?是的getOtherPeer方法其实就是用于获得对方的RTCPeerConnection对象,因为我们的 Demo 是在同一页面创建的。所以不需通过其他载体交换。

好的,说完连接创建,我们接着说创建offer。在创建offer前,我们已经留意到,其实已经把本地的视频流添加到RTCPeerConnection对象中了,因此offer所带的SDP会话描述,已经带有相关信息。我们先来createOffer 成功后的回调方法。

// 创建 offer
function createdOffer(description) {trace(`Offer from localPeerConnection:\n${description.sdp}`);trace('localPeerConnection setLocalDescription 开始.');localPeerConnection.setLocalDescription(description).then(() => {setLocalDescriptionSuccess(localPeerConnection);}).catch(setSessionDescriptionError);trace('remotePeerConnection setRemoteDescription 开始.');remotePeerConnection.setRemoteDescription(description).then(() => {setRemoteDescriptionSuccess(remotePeerConnection);}).catch(setSessionDescriptionError);trace('remotePeerConnection createAnswer 开始.');remotePeerConnection.createAnswer().then(createdAnswer)
}复制代码

简单明了,对于localPeerConnection来说是本地,所以就是调用 setLocalDescriptionoffer信息存储。而对于对方就是远程remotePeerConnection就是用setRemoteDescription进行存储了。这里跟我章节前说的第4步说的不一样,这里没有转成字符串。聪明的同学可能猜到为什么了,因为这里是同一个页面,不需要传输呀。

紧接着马上remotePeerConnection就调用createAnswer创建了一个 answer,让我们继续看,

// 创建 answer
function createdAnswer(description) {trace(`Answer from remotePeerConnection:\n${description.sdp}.`);trace('remotePeerConnection setLocalDescription 开始.');remotePeerConnection.setLocalDescription(description).then(() => {setLocalDescriptionSuccess(remotePeerConnection);}).catch(setSessionDescriptionError);trace('localPeerConnection setRemoteDescription 开始.');localPeerConnection.setRemoteDescription(description).then(() => {setRemoteDescriptionSuccess(localPeerConnection);}).catch(setSessionDescriptionError);
}
复制代码

这里跟上面的createOffer回调做的差不多,把answer存储到双方对应的描述中。

到这里为止双方的连接建好,offeranswer也存储妥当。由于remotePeerConnection在之前已经已经注册好addStream的响应方法了gotRemoteMediaStream,而正如前文说的,因为创建offer的时候已经把视频流带上了,所以gotRemoteMediaStream此刻会回调,通过这个方法,把视频流显示在remoteVideo标签中。

// 回调保存远程媒体流对象并把流传到 video 标签
function gotRemoteMediaStream(event) {const mediaStream = event.stream;remoteVideo.srcObject = mediaStream;remoteStream = mediaStream;trace("远程节点链接成功,接收远程媒体流中...");
}
复制代码

现在,我们应该可以看到两个一模一样的画面了。注意哦,右边那个是通过RTC 传输过来的。撒花~

这一篇先到这里吧,我们下一篇继续。下一篇会继续继续深入WebRTC架构和ICEsignling之类的内容。谢谢大家的阅读,毕竟我也是个初学者,如果文中有不对的地方,大家可以评论一下,然后一起探讨。再次谢过。

代码和参考文档

  • DEMO-1 代码
  • DEMO-2 代码
  • 官方文档
  • 官方 codelabs
  • Interactive Connectivity Establishment
  • Session Description Protocol

Agora SDK 使用体验征文大赛 | 掘金技术征文,征文活动正在进行中

转载于:https://juejin.im/post/5cbc8b2cf265da03ab23267d

一起来学习 WebRTC (篇一)| 掘金技术征文相关推荐

  1. webrtc+canvas+socket.io从零实现一个你画我猜 | 掘金技术征文

    开场白 最近键盘坏了,刚好看到掘金有声网的技术征文,想整个键盘.于是就开始从零开始学习webrtc, 一开始看文档就是个素质三连.这么难啊,这咋整啊,这谁顶的住啊.于是就开始全网找资料,很幸运的在掘金 ...

  2. Flutter完整开发实战详解(二、 快速开发实战篇) | 掘金技术征文

     作为系列文章的第二篇,继<Flutter完整开发实战详解(一.Dart语言和Flutter基础)>之后,本篇将为你着重展示:如何搭建一个通用的Flutter App 常用功能脚手架,快速 ...

  3. Spring Boot干货系列:(六)静态资源和拦截器处理 | 掘金技术征文

    原本地址:Spring Boot干货系列:(六)静态资源和拦截器处理 博客地址:tengj.top/ 前言 本章我们来介绍下SpringBoot对静态资源的支持以及很重要的一个类WebMvcConfi ...

  4. Flutter 底部导航——BottomNavigationBar | 掘金技术征文

    前言 Google推出flutter这样一个新的高性能跨平台(Android,ios)快速开发框架之后,被业界许多开发者所关注.我在接触了flutter之后发现这个确实是一个好东西,好东西当然要和大家 ...

  5. Flutter实现动画卡片式Tab导航 | 掘金技术征文

    前言 本人接触Flutter不到一个月,深深感受到了这个平台的威力,于是不断学习,Flutter官方Example中的flutter_gallery,很好的展示了Flutter各个widget的功能 ...

  6. 9月,水了几个大中厂前端面试的一些总结分享 | 掘金技术征文

    写在前面 工作吧,我觉得就像谈恋爱,不一定是找高富帅或者白富美,互相确认过眼神是对的人就可以~而面试的自信和对工资的要求,源于你过硬的基础和平时的思考.积累以及总结~ 8月底离职,其实是裸辞,当然大概 ...

  7. 腾讯面试后续 | 掘金技术征文

    前言 在春招过程中,参加了不少公司的面试,这次就继续上次说的腾讯春招吧.之前提前批收到了一次面试,但后来就没有消息了,应该是妥妥的挂了.接着走正常渠道,参加笔试,笔试之后过几天,收到了通知,参加线路面 ...

  8. Flutter入门三部曲(3) - 数据传递/状态管理 | 掘金技术征文

    Flutter数据传递 分为两种方式.一种是沿着数的方向从上向下传递状态.另一种是 从下往上传递状态值. 沿着树的方向,向下传递状态 按照Widgets Tree的方向,从上往子树和节点上传递状态. ...

  9. 奉献一波鹅厂的面经!纪念最后的校招!| 掘金技术征文

    薪资:special offer(已收到正式offer) 个人情况:主要做后台研发方向(c/c++/java)都用过,末流985计算机专业硕士,有过长达一年半的实习工作经历.计算机基础,算法,项目都还 ...

最新文章

  1. Jmeter 使用自定义变量
  2. 【优秀作业】蚁群优化算法
  3. 【集合论】二元关系 ( 二元关系运算示例 | 逆运算示例 | 合成运算示例 | 限制运算示例 | 像运算示例 )
  4. 《化工原理》基本知识点
  5. 【cJson】 JSON格式详解
  6. Photoshop图层学习总结
  7. JavaFX技巧12:在CSS中定义图标
  8. 使用matlab工具研究神经网络的简单过程(网络和数据下载)
  9. 数据科学家应该掌握的12种机器学习算法(附信息图)
  10. python--过滤top命令--之--时间_系统CPU_进程CPU_内存
  11. 推荐一本go语言入门书籍
  12. Java解析JSON大文件解决方案之JsonReader
  13. 按需使用vue-cli-plugin-element插件
  14. 科技爱好者周刊(第 150 期):当音乐还是稀缺的时候
  15. Arduino ESP32 看门狗定时器
  16. 《炬丰科技-半导体工艺》 自对准栅氧化镓金属氧化物半导体晶体管
  17. 深度:从 Office 365 新图标来看微软背后的设计新理念
  18. 【Mysql笔试】-常见笔试题汇总
  19. Electron在win7上加载plotyjs失败的解决方法
  20. 突破性进展什么意思_宣布突破性发展2011

热门文章

  1. 《3D数学基础》系列视频 1.5 向量的夹角
  2. ArchLinux下LXDE的安装与设置心得
  3. 毕业设计:基于SSM实现新生报道系统
  4. RHEL5.6配置本地yum源
  5. 数据库锁解决并发问题
  6. sublime Text3快捷键使用大全
  7. 算数运算符/空值问题
  8. getOutputStream() 的问题
  9. CSU 1328: 近似回文词
  10. GDI+ 中Image::FromStream ,用流的方式显示图像