主页 > 呼叫外包 > 行业新闻 > RTE 大会回顾 | 美团 WebRTC 电话终端工具实践

RTE 大会回顾 | 美团 WebRTC 电话终端工具实践

POST TIME:2021-10-14 21:10

近几年,基于WebRTC的电话终端工具在通讯行业中越来越流行,客户服务可以直接通过浏览器拨打电话来实现。目前业内大多数Web电话工具仅支持单个页面使用,无法支撑美团多业务复杂的外呼场景,美团在WebRTC领域不断探索,实现了多页面多域名共用的Web端电话SDK。在 RTE 2020 实时互联网大会上,美团前端技术专家杨尚林分享了美团是如何通过共享线程来解决多页面多域名下共享通话状态的业界难题的。
 
▶️点击「阅读原文」可观看视频回放,获取 PPT
 
以下为演讲实录:
 
大家好我是来自美团网的杨尚林,很高兴来参加今年的实时互联网大会,在这里我将也会跟大家交流一些我在美团这边一些日常的工作。
 
今天我主要跟大家分享的题目是美团的webRTC电话终端工具实践。下面开始我今天的分享,我今天的分享将会进行五个部分讲解,分别是工具介绍、项目背景、项目目标、实现方案以及最后的项目成果的展示,希望跟大家一起进行探讨。
 
首先我会先介绍一下我们的终端工具到底是个什么样的东西,大家首先可以看到一个demo的展示,那么相信在很多做通信公司里面都有的,很多公司都会做自己内部电话的系统,它其实本身是包括外呼、接听、转接、挂断、呼叫保持、通话计时、通话质量检测,最后其实还包括一个很重要的多页面的使用,我不知道各位公司里面会不会有这样的功能。看完了它是什么之后,我们来看一下它的项目背景是什么,首先目前来说各个目前各大公司其实都有自己的云呼叫中心,或者有自己内部自建的通信系统,那么它本身是需要提供一些基础的通话能力和坐席能力,比如说最基本的语音通话能力、坐席、技能、队列等等能力,包括智能IVR、机器人的能力,同时它还会提供一套客户关系管理或者工单系统,那么会兼容其他的数据报表等系统,包括知识库。
 
其实我们可以看到语音通信能力是语音呼叫中心最基础最基本的能力,语音通话能力一般情况下怎么样去做呢。
 
现在的做法是说去实现一个桌面的软件化,比如说我们平时经常用的ESPhone/X-Lite,在PC端的情况下,那么在Web端我们一般依赖于WebRTC,比如说我们会实现一个电话条的工具或者一个单独的页面,给我们业务方去提供使用。
 
那么Web电话一般的使用场景是什么呢,首先我们会用一种电话条UI组建的方式去嵌入业务方的页面中,这个页面可能包括工单系统CRM等,它的功能包括电销、回访、客服这种常见的功能,因为它需要满足我们正常的通话操作。比如说外呼、拨号、挂断等等,它还需要去保持我们的通话状态,我们的坐席是需要实时的感知到电话的状态的,比如说通话中、挂断、忙线等。其实还有一些非常必要的功能。
 
目前来说我发现市面上其实很多的电话功能是不具备的,比如说多页面、多系统、同状态,这三者分别是什么意思呢,第一点就是其实我们的业务方式需要在多个浏览器标签里进行外呼和挂断的,其实目前来说大多数的这种电话工具都只是在单个tab页里去发起外呼和接听,或者只支持单个tab页的注册动作。这个需求是重点,就是说业务方其实是不关心自己的tab页的行为是什么样的,用户也不需要关心自己所做的操作是什么,他们所关心的其实只是日常的工作,比如说他们接打电话、处理工单、回访用户等一系列。同时这个业务方还可能使用多个系统,那么他们在真正接入的时候在对于同一个业务方的两个系统来说,一个是工单系统,一个是CRM系统,他们所需要的接入信息是一样的,因为这个坐席它可能是同时使用两个系统的,那么当一通电话呼入的时候它不可能说只是某一个页面进行响应,或者是两个同时响应,它是需要同时两个响应的话接起任意一个,所以说业务方式可能使用多个系统的,每个系统都需要具备外呼的能力,在这种状态下这种同页面多系统状态下的,每一个标签页都需要保持统一的状态。
 
比如说我们在A页面进行外呼的时候,我们这时候切换到B页面把它挂断到,其实电话所做的操作是一样的,A、B页面的电话都会被挂断掉。
 
像这里就是展示了一个我们平时内部的一个demo页,我们可以在多个tab页内保持通话状态,并且在多个tab业同时进行通话。本次这个项目目标我们是针对前面我们前面我们需要的功能去做一些分析。
 
第一个我们必须支持多页面的使用,当然除了基本的电话功能之外,那么它支持多页面多系统同时注册和使用,并且使用的注册信息是相同的,比如说我们使用同一个分机号去进行注册。
 
第二个多页面、多系统的时候状态是需要完全同步的。
 
第三个它的业务接入是需要非常简单,因为我们的业务方它仅需要引入我们的组件就可以接入我们的工具了,而不是要关注我们为多页面多系统开发的成本所带来的变化。比如说它不需要为我们支持他们进行多页面而进行任何的适配工作。
 
第四个就是我们的用户体验必须是良好的,首先保持良好的UI和交互,要有完美的错误回调和说明,也要提供很好的质量检测与设备探测功能。
 
我们在实现这个电话条中所经历的哪些过程,首先这个电话工具它其实是有三个难点的,第一个就是多Tab业注册是很难的,第二个多连接管理是非常难的,第三个我们会给大家介绍一个新的方案,引入这个新的方案我们带来了很多相关的问题。
 
首先我们看一下多Tab业为什么注册难,我想这个问题大家都是非常清楚的,因为我们都是有共性的,我们知道在WebRTC SDP交换过程中,首先我们是需要对端的地址和端口号的,我们的媒体服务器其实会为每一个注册的动作去事先分配好一个IP+端口号去用于日后的传输。如果说我们有多个Tab页的时候,比如说当Tab A已经完成了注册并且已经完成了SDP的交互,并且在建立语音的情况下,就是说我们的媒体服务器已经为他分配好了一个端口号去进行UDP的传输,我们的Tab B又发起了注册,这个时候我们的媒体服务器又会为Tab B重新分配一个端口号去用于后续的传输,其实这个时候我们媒体服务器用于中转语音流的端口其实是已经变化了,就会导致当前的通话直接被断掉。
 
在我们日常场景中实际上是面临这个问题,我理解其实我们日常生活工作中所面临的问题是大同小异的,所以说其实我们的结论在我们美团内部的结论是显而易见的,多个Tab页其实我们只能为他进行一次的注册动作。
 
那么其实我们怎么样让多个Tab只进行一次注册动作呢,其实我们在这之间去引用sharedWorker,那首先我们先不去sharedWorker是什么,我们首先来看一下shared Worker在其中起到的作用是什么,我们目前来说,假如说我们有两个Tab页的话那么shared Worker在中间起到一个桥梁的作用,它会沟通两个Tab页,它会为两个Tab页之间建立一定的联系,并且我们在Tab页中间只保持一个sharedWorker,我们仅通过这个sharedWorker在我们的远端发起单个连接,可以理解为TabA和TabB它们两个共用了同一个连接。
 
这个时候这样的话我们的注册命令其实就很好实现,因为在我们看来我们的电话工具其实是只有一个,我们其实不需要关心我们有几个Tab页,Tab A和Tab B目前来说对我们来说都已经只是一个页面了,我们只需要通过sharedWorker去发起同步的操作就可以了。
 
那么sharedWorker是什么呢,它是一个共享多线程,它本身代表了一种特定类型的Worker,可以从浏览器的上下文中访问,比如说在几个窗口内iFrame或者其他的Worker中,它本身是有自己的全局作用域的,并且它还使用Post Message进行通信的。
 
它其实是有很多限制,比如说同源限制,那么我们其实规定加载的Worker脚本必须与我们的宿主页面是同源的,并且shared Worker在连接不同页面的时候,这些页面也必须是同源的,就是说天然规定的我们所接触的业务方它的域名必须是同源的,请注意我们之前提到过,我们是支持业务方使用多系统的,并且这些系统可能是不同域名的,后面会介绍我们怎么去处理这种现象。
 
第三个就是作用域是无法访问DOM的,并且它无法在共享线程中使用任何WebRTC的相关API。7
 
下面有一个sharedWorker使用的例子。它的兼容性其实在我们看来是非常好的,目前来说在不考虑IE的情况下它的兼容性基本上与WebRTC相关的生态是同步的,并且国产浏览器同样是支持,在国内环境中。唯一的问题是隐身模式是无法使用的,但是这个其实不是问题,因为它和localStorage这些特性其实是一样的。
 
首先我们来看同源限制,为什么我们需要去考虑这个同源限制呢,因为毕竟我们作为平台方的话我们所有的资源脚本都是要去给业务方引用的,所以我们势必会被为业务方提供一个npm包或是一个JS的脚本,这个时候势必会与业务方的域起冲突,所以我们一定要想办法解决这个同域的限制,我们这里研究出来两个方法,分别一个是Data URL,一个是iFrame。
 
Data URL其实我们之前用过,比如说我们在引用Base64的时候其实都是去使用过的。我们怎么样去使用它呢,我在下面也给出了一个例子,是通过本身的importScripts API去引入官方域下的sharedWorker脚本。
 
iFrame方案其实通过iFrame代理去进入sharedWorker脚本。举个例子,我有两个域名,分别是A域名和B域名,我其实是无法再两个域名中间去建立使用同一个shared Worker的,但是我可以在A和B两个域名中引用同样域的iFrame,我们使用同样域的iFrame去引入同样的sharedWorker,那这个时候其实我们就间接的让A和B共同的使用了shared Worker的脚本,之后我们再配合post Message进行消息中转就可以了,相当于iFrame在中间起到代理层的作用。
 
我们看一下架构图,针对单个域名的时候sharedWorker就是一个代理,我们是通过Data URL这种方案去引用的,意思就是我们使用sharedWorker的脚本地址是一个Data URL地址,之后它会为我们去建立连接,跟我们之前说的流程是完全一样的。
 
其实很多人会担心这种Data URL方式会不会更多的是一种hack,其实我们之前也是有担心的,就是在我们比如尝试摸索出Data URL这种方式之后,其实我们是专门去查证的原码,最后是发现chromium是专门放开了Data URL这种跨域的限制,原码也在这里,大家可以放心去使用的,因为这条commit是可以加上去的。
 
那么iFrame方案其实更多的是针对多个域名的情况,比如说像之前讲到的我们存在两个域名,我们可以同时引入相同域的官方iFrame脚本,并且引入同样的sharedWorker去达到我们不同的域名不同页面使用同一个sharedWorker的目的,并且通过这个sharedWorker帮我们做资源的分发连接,那么这里面其实与Data URL相比iFrame,我们为什么会有两个方案呢。就是因为iFrame其实大家都是比较清楚的,它相对来说耗费的资源要相对多一点,它的速度要相对慢一点,但是在绝对速度上来说其实也是在毫秒级别的,所以说业务方其实可以是自行选择去采用多域名或者单域名的配置,我们仅仅是通过一个配置项就可以给他们提供到。这里面相对来说我们如果说业务方只有单个域名的话使用Data URL这种方案可能会稍微快一点。
 
其实在这个方案的背后我们是有很多的问题的,这些问题其实是非常头疼也是非常必要的,我们现在看一下我们为了支持我们的多页面会引入哪些问题,并且我们是如何解决这些问题。
 
我目前总结的这些问题应该是支持多页面里面并且是sharedWorker方案里面应该是最棘手的问题了,底下分成三部分去讲解。
 
首先我们就会面临一个通话页异常关闭的现象,那么它的现象是什么呢,就是因为我们的sharedWorker,我们之前讲过它其实是无法使用WebRTC相关生态里的任何对象的,比如说MediaDevices,也就是说明我们的WebRTC相关设备连接逻辑必须是存在于我们业务方系统的某一个页面内。所以我们势必会出现,当我们把那个页面关闭掉的时候,如果业务方使用多个页面,那其实多个页面的整个这种呼叫都会被挂断掉。
 
比如说我们关闭了发起外乎或者接听的这种页面的话,页面的流就会被断掉,下面有一个示意图,其实我们任何的这种多页面的情况下,每时每刻都只有一个页面的Web RTC其实真正的逻辑是在工作中的。它的原因是很明显的,比如说我们在进行呼入的时候其实我们会在直接外呼的页面上建立peerConnection的连接。比如说我们存在呼入的时候其实我们会在接听的那个页面上去进行peerConnection的连接。
 
下面是有例子的,比如说我有三个订单页,其实当我的订单A是真正的WebRTC承载页,那如果说我不小心把它关掉了,我们大家想一下真实逻辑的处理流程,比如说我是一名客服,我要处理三个订单,我很容易在跟一个用户咨询完电话之后我不小心,或者说在中途中,其实我切换了好多页面,我是不小心把某个页面关掉了,但是恰好这个页面是我真正WebRTC承载的页面,这个时候其实整个电话都会挂断掉。这个只是我个人一个不小心的行为会导致整个页面挂掉,但是我们不能把这个问题去归咎去让客服同学注意这种行为。
 
其实我们是需要给一定解决方案的,这个解决方案其实是比较简单的,我们只需要去给到我们的开发同学一个正在通话的状态就好了,我们让他根据这个通话状态去进行一个提示,比如说通话中我们不允许他关闭页面,当然这种方法是非常简单非常高效的。但是其实我们有另外一种方法去完全规避这种开发的问题,这个时候我们会增加一个设备页面,设备页面它其实是用来创建WebRTC实例,并且建立peerConnection连接的页面,其实这个设备页面在我们提出这个方案之前它是存在一个业务方的逻辑里面的,就是业务方的页面里面的,虽然跟我们的SDK在一起,最终我们是希望我们增加这个设备页面去始终通过这个设备页面去管理我们的语音,并且承载我们的通话。
 
我们看打开这个设备页面的逻辑,设备页面建立的时机是什么呢,比如说我们有呼入或者来电的时候,我们首先会去检查设备页面是否存在了,如果没存在我们会把设备页面拉起并且建立起来,并且建立它的WebRTC相关的逻辑。
 
他打开的策略是这个设备页面会以最小的窗口进行打开,比方说我们给它高100像素的一个大小,并且它会显示电话的状态,并且它会置于所有的tab页之后,平时是不可见的。我们还会做一个逻辑,比如说当我们的业务页面全部关闭的时候,我们会自动触发关闭这个tab页的操作,其实用户他不会感知到我们这个设备页面的存在。就好比他是运行在所有tab页之后的一个幽灵一样。所有的操作同学他其实可以自由的使用他的页面,关闭任何一个页面都不会影响到设备页面,只有说当我们把所有的页面全部关闭之后才会对设备页面进行关闭的操作。
 
那么现在我们其实可以看一下整个的架构,因为我们其实是引入了sharedWorker,其实正是因为我们有sharedWorker我们才得以去实现这种设备页面的这种操作,设备页面其实与业务页面他们之间都是通过sharedWorker去进行共享电话的状态和操作的,比如说业务页面进行电话的这种状态的展示,设备页面去执行语音的动作,其实我们可以理解为每一个页面包括设备页面包括业务页面他们都是可以从sharedWorker中获取一定信息的。
 
当然只是说他们执行不同的动作而已,而只有设备页面去承载语音的质量,而且他是唯一承载语音质量的页面。
 
那么我们还会面临版本升级混乱的问题,怎么去理解这个问题呢,其实可能多数没有使用过sharedWorker同学完全想象不到这个问题的,但是这个问题在我们的升级开发中是非常非常严重的问题,因为他会导致我们整个电话系统不可用,并且每次当我们发布新版本的时候都会存在这样的问题,就是我们升级一个新版本,业务方却完全不可用了,它是怎么回事呢,当我们的sharedWorker脚本进行升级的时候,也就是对我们的SDK进行升级的时候,我们是需要对版本进行区分的。
 
因为sharedWorker它原理是跟webWorker一样的,就是我们如果想要对它进行替换的话,我们需要去改变它的URL,所以我们需要用不同的版本对它的资源加以区分才可以替换。
 
比如说我们想把0.0.1的Worker去换掉0.0.2的Worker,我们真正期望的是多个页面共享的sharedWorker,它其实能与正常的前端发布一样,用户是不需要关注页面刷新或者说资源发布时机的,它可以正常的使用,完全无忧无虑的去用,我们来看一下这个问题是怎么造成的呢,其实在升级版本之前用户的版本都是统一的。
 
比如说Worker的状态其实也可以共享一例,所有的状态都是可以同步的,比如说我们可以看一下左图,当我们没有升级版本之前大家的版本都是0.0.1,包括sharedWorker它本身的脚本也是0.0.1。那我们升级版本之后用户刷新了某个页面会导致这个页面版本升级,从而引入新的Worker脚本,这样会导致多个页面间状态是无法同步的。
 
因为我们底层出现了两个Worker脚本,它会与我们的远端进行两个连接,并且他们的上层所有这些业务页面是无法再进行通信了,相当于我们又开辟出来一个新的业务页面,他们之间再也没有关系了。这个例子大家是可以理解的。
 
比如说我们三个页面中某一个页面刷新了它变成0.0.2了,它势必会引入新的shared Worker的资源,也变成0.0.2,这个时候其实我们底层的shared Worker会创建两个事例,因为它们不再是同样的URL了。
 
我们怎么去解决这个问题呢,其实我们需要引入一套shared Worker这种本身的升级机制的,去避免这种多页面版本升级时和这种刷新动作带来的这种不可用的现象,所以说我们可以通过我们总结出来的流程,去实现Worker的这种自我检测和自我升级的。首先其实我们在运行过程中,我们是需要时时刻刻知道我们当前的版本是什么。
 
比如说我们在我们打包的过程中,把我们当前的版本给打入到我们的包里面去,在我们运行的过程中,其实我们在加载的初期是时刻需要对我们的Worker版本进行探测的。首先我们需要感知到当前在活着所有的Worker它是一个什么样的版本,而我们刷新之后我们新引入我们希望升级的Worker是一个什么样的版本,其实我们怎么样在我们刷新页面的时候,知道我们当前活着的就是其他页面引用的Worker是什么呢。
 
因为这个时候我们还没有建立这个shared Worker连接,其实我们是无法进行交互的,所以我们是需要先引入原先的shared Worker首先进行一个探测,去探测我们其他的页面,目前的shared Worker是一个什么样的状态,同时我们需要把这个状态去存入我们的localStorage之中。就好比是,如果我们进来之后发现我们当前已经使用了localStorage里面某一个版本的版本号的话,那么我们会跟自己的状态自身版本号进行对比,自身版本号发现不一致的情况下,我们会给它进行一个升级并且替换,这个升级替换规则是什么呢。
 
比如说我当前是0.0.1我需要把自己替换成0.0.2,首先我需要同样先引入0.0.1的这个sharedWorker,这样的话我才能跟其他的页面引入的sharedWorker的版本是一致的,我们大家都引入0.0.1的版本,这个时候sharedWorker是保持一列的,之后我再传入一系列命令,去告诉我的sharedWorker你要把你自己替换成0.0.2的版本,然后shared Worker会先所有业务页面同步状态,所有的业务页面都会把自己对0.0.1版本的shared Worker的引用销毁掉,并且把自己升级成0.0.2的版本。我们目前建立一种shared Worker自更新自升级的机制。
 
如果其实想实现一个Web电话工具,我们是需要有其他优化内容的,比如说我们需要去提供一些懒加载机制,避免业务方自身的懒加载。
 
第二我们需要一系列设备检测,帮助我们客户快速的定位问题,去检测他们的设备当前是否可用的,语音质量是如何的。
 
第三个,我们需要去提供一些网络检测的工具或者说页面,或者最好把它集成到我们的电话工具里面去,还需要对我们云监控质量进行一个通信的大盘,还需要给用户在通话过程中进行一个Notification的提示,包括可视化的一个质量信号实时的抖动。
 
还有我们每通电话它通信过程中发生的一些问题,如何快速定位问题,进行一个可视化的链路的监控。
 
如果说在这个链路监控中出现问题的时候,我们还需要对我们的操作人员或者说我们的开发人员进行一个实时的告警。
 
这里其实为大家展示了一个整体的架构(如上图所示),它其实会包括很多的东西,因为shared Worker是我们整个方案的核心,整个方案里面不光是有sharedWorker,其实它包含了很多内容,所以工程化的能力其实也是需要去思考的,这里面也有我们的架构图可以给大家展示一下,最终我们其实是达成了一些成果的,有些成果是在我们意料之内,有些成果其实是在我们完成之后又去发现的,首先我们的业务收益其实是目前支撑了我们美团数千名坐席的日常电话服务。包括电销、面试、销售等各种各样场景。
 
其实它是同时满足业务方快速接入的,业务方其实只需要引入我们的资源就可以了,并且它执行一个初始化的方法,他是不需要关心多个页面带来的问题的,接入是非常简单的。
 
其实它是有一些技术收益的,比如说我们去引入sharedWorker这个方案之后,大家知道我们是保持单个连接,单个连接大大的降低了我们后端系统复杂度的,比如说我们不需要再用MQ去同步机器之间的状态,我们不需要再去维护多个链接同步状态去进行复杂的逻辑。
 
第二个我们是大大降低了并发的,不光是前端的连接,包括后端需要同步状态,MQ连接,包括数据库查询连接,包括我们Redis的连接等各个查询连接,我们都是降低了很多并发流量的。
 
第三点我们知道,我们本次的方案是多个tab页我们只引用了一个连接,其实我们的日志是只需要打印一路就可以了,多个tab页其实对于我们来说更像是只存在一个电话,所以我们的日志也只需要一路了,否则的话我们需要打印多路日志,这些日志其实是有大量的冗余的,并且我们查起来其实所有日志的事件是交错在一起的。
 
以上就是我本次的分享,本次分享里我也介绍了我们美团在电话业务上的一些实践,为大家介绍了sharedWorker这种方案,其实sharedWorker这种方案不光是在电话的场景中,其实在日常工作中,比如IM系统中也是可以去使用的。

标签:迪庆 鞍山 咸宁 游戏 内蒙古



收缩
  • 微信客服
  • 微信二维码
  • 电话咨询

  • 400-1100-266