引言

富媒体是指在即时通信过程中传输的图片、语音、视频、文件等媒体介质的展示方式。

一、背景

客服一站式平台旨在为得物生态内的客服域服务人员提供一站式的服务办公平台。我们有多条业务线,客服在和用户聊天的过程中,有很多场景需要发送富媒体。跟普通的文本传输相比,富媒体可以直观的让用户了解到消息内容,但是在传输过程中也面临着文件大、内存消耗大、传输过程漫长等问题。

二、面临的挑战

客服发送大文件(视频、图片)等消息给用户的大致流程如下:

首先通过文件上传服务上传到CDN,同时返回对应的CDN地址链接;其次是获取到CDN地址链接,通过IM网关将链接返回给用户界面渲染。

在整个传输过程中,前端必须等文件上传成功拿到链接之后,才能渲染,如果传输的文件很大,客服需要会等待很长时间,这对于客服的接线效率有非常大的影响。比较理想的方式是当客服发送文件的时候,文件立马在聊天窗口渲染,此时渲染的不是完整的文件,而是文件的画像,比如文件的名字、封面图片,通过消息的状态进行上传状态的控制。

以视频传输为例,如果直接把视频放在缓存中展示在客服聊天内容区域,庞大的缓存会让用户的浏览器分分钟崩溃。比如大于70M的视频,在网络,电脑硬件等环境都较好的情况下,从读取文件到获取到首帧图片传输的过程大概需要2~3s,如果在网络一般,同一环境下有多人在发送视频文件,或者硬件设备一般的情况下时间会更长。

如何在不影响客服接线效率的情况下,还能让大文件的传输做到如丝般顺滑呢?

三、解决方案与成效

1、将fileReader.target.result作为视频的url在页面渲染

最初使用的方式是在视频上传CDN时,同时截取视频首帧,然后将截取的视频首帧也上传到CDN,再通过长链(wss)发送给客户端,因为截取首帧是一个同步的过程,需要拿到screenshot的url之后才能渲染到页面,导致客服在点击发送的第一时间在聊天界面看不到发送出去的视频,如上图视频所示,客服无法感知到视频发送的进度。

通过FileReader读取文件信息:

export function getFileInfo(file: File): Promise<any> {return new Promise((resolve, reject) => {try {const reader = new FileReader()reader.readAsDataURL(file) reader.onload = (event: ProgressEvent<FileReader>) => {resolve(event)}} catch (e) {reject(e)}})
}

通过返回的文件信息进行属性设置:

export function getVideoInfo(file) {return new Promise((resolve, reject) => {getFileInfo(file).then(fileReader => {const target = fileReader.target.resultif (/video/g.test(file.type)) {const video = document.createElement('video')video.muted = truevideo.setAttribute('autoplay', 'autoplay')video.setAttribute('src', target)video.addEventListener('loadeddata', () => {// ...})video.onerror = e => reject(e)}}).catch(e => reject(e))})
}

如上代码 video.setAttribute('src', target),如果用target作为视频的url在页面渲染,页面会分分钟崩溃。可以看一下1M的视频文件,通过readAsDataURL(file)读取文件内容得到是一个data:url的base64字符串,用这个字符串进行渲染,等于在页面加了一个1.4M的字符串内容,如下图所示,这样做的后果不可想象,在文件稍微大一些的话会有更加明显的卡顿。

所以这个方案在开发之初就被否定了。

2、采用的URL.createObjectURL(file) 获取到URL

在第一种方案被否定之后,又调研了URL.createObjectURL的实现。采用的URL.createObjectURL(file) 获取到URL(这个URL对象表示指定的 File 对象或 Blob 对象),然后放到聊天数据的缓存中,便于快速发送到客服聊天窗口页面。其主要实现代码如下:

if (/*******/) {// ...//. blob作为预览视频的urlstate.previewVideoSrc = URL.createObjectURL(file)state.previewVideo = truestate.cachePreviewVideoFile = filenextTick(() => {focus()})} else {// ...}

经过这个改造很明显的看到视频发出之后,可以很快的展示在页面上,让客服感知到视频发送的状态和进度,相对于方案一,视频发送的过程有明显的提升。渲染出来的代码效果如下图所示:

但是!

在给客户端发送视频信息时,要携带首帧和视频时长,作为展示封面,历史的做法是:
首先前端获取文件信息后通过canvas转换成图片再上传到CDN;在获取到首帧和文件信息之后,先上传到CDN,返回URL后再通过长链发送给用户,同时更新页面的URL地址为CDN返回的真实地址。

取首帧时要读取文件,既然是读取文件,还是存在一定的耗时,如下代码片段所示,这段耗时任务也会影响到客服的使用体验。

export function getVideoInfo(file, msgid?: string) {return new Promise((resolve, reject) => {getFileInfo(file, msgid).then(fileReader => {const target = fileReader.target.resultif (/video/g.test(file.type)) {const video = document.createElement('video')video.muted = truevideo.setAttribute('autoplay', 'autoplay')// target只作为url创建视频用于获取视频大小、播放时长等基本信息,不用于页面渲染video.setAttribute('src', target)video.addEventListener('loadeddata', () => {const canvas = document.createElement('canvas')canvas.width = video.videoWidthcanvas.height = video.videoHeightconst width = video.videoWidthconst height = video.videoHeightcanvas.getContext('2d')!.drawImage(video, 0, 0, width, height)const src = canvas.toDataURL('image/jpg')const imgFile = dataURLtoFile(src, `视频_${Math.random()}.png`)return getImgInfo(imgFile, fileReader.msgid).then(({ width: imgWidth, height: imgHeight, file: imgFile, size: imgSize, src: imgSrc, msgid }) => {resolve({//  ...})})})video.onerror = e => {// ...reject(e)}}}).catch(e => {reject(e)})})
}

上传视频的时候,文件服务器提供了获取首帧的方式拿到首帧图片,在链接地址上拼接对应的参数即可,如下所示:

// 拼接的获取图片首帧的URL地址
export const thumbSuffix = `?x-oss-process=video/snapshot,****`
export function addOssImageParams(url, isThumb = false) {const suffix = isThumb ? thumbSuffix : urlSuffixif (!url) return ''// ...return url
}

但在实际的使用场景中,只获取视频首帧信息是不够的,还要获取视频的宽高、播放时长等信息,并且通过网络请求发送给网关,最终在客户端展示。读取文件这个过程无法避免,耗时问题还需要解决。

3、Web Worker异步读取文件信息

通过方案二虽然实现了文件的快速渲染,但读取文件信息如果在浏览器的主线程去做,耗时长的话,还是会阻碍客服的操作。如果这个过程能通过异步去实现,那就很完美了。JS虽然是单线程,但是浏览器提供了Web Worker的能力,让JS也能通过异步的方式和主线程进行通信。首先对比下浏览器主线程执行和主子线程执行的区别,如下图所示:

  • 浏览器主线程在执行发送文件的时候,如果发送文件任务没有结束,则会阻塞其他的任务,相当于发送期间,客服什么事情也做不了;
  • 浏览器主子线程在执行发送文件的时候,通过子线程读取文件,在读取文件期间,主线程可以继续执行其他的任务,等到子线程读取完文件通过postMessage发送相关的信息告知主线程文件读取完毕,主线程再开始渲染。整个过程对于客服没有任何阻塞。

Web Worker主子线程实现的流程如下:

首先在线程订阅中心创建子线程任务,如下:

// 子线程任务
export function subWork() {self.onmessage = ({ data: { file } }) => {try {// 读取文件信息// ...// 发送对应信息self.postMessage({ fileReader: **** })} catch (e) {self.postMessage({ fileReader: undefined })}}
}

然后在线程订阅中心初始化Worker,如下:

export const createWorker = (subWorker, file, resolve, reject) => {const worker = new Worker(URL.createObjectURL(new Blob([`(${subWorker.toString()})()`])))// 发到子线程worker.postMessage({file})// 监听子线程返回数据worker.onmessage = ({ data: { fileReader } }) => {resolve(fileReader)// 获取到结果后关闭线程worker.terminate()}// 监听异常worker.onmessageerror = function () {worker.terminate()}
}

最后在主线程里面调用Worker获取文件信息,如下:

// 创建主线程任务
export const getFileInfoFromSubWorker = files => {return new Promise((resolve, reject) => {createWorker(subWork, files, resolve, reject)})
}

通过上面的三个步骤,基本就可以在不影响客服操作的情况下获取到文件信息。获取到视频信息对象之后,再通过URL.createObjectURL(file)即可获取到视频相关的属性信息,如下:

export function getVideoInfo(file, blob, msgid?: string) {return new Promise((resolve, reject) => {if (/video/g.test(file.type)) {const video = document.createElement('video')video.muted = truevideo.setAttribute('autoplay', 'autoplay')// blob作为url: URL.createObjectURL(file)video.setAttribute('src', blob)video.addEventListener('loadeddata', () => {const width = video.videoWidthconst height = video.videoHeightresolve({videoWidth: width,videoHeight: height,videoDuration: video.duration * 1000,videoFile: file,videoSize: file.size,videoSrc: blob,msgid})})video.onerror = e => {reject(e)}}})
}

如上所述,在获取文件对象信息之后,再通过blob的方式直接获取视频的宽高作为第一帧图片的宽高,二者结合即达到了在不影响客服操作的情况下,让视频发送做到了如丝般顺滑。

通过Web Worker+URL.createObjectURL(file) 的方式,解决了富媒体文件发送时,不管有没有发送成功,都可以实现秒发的效果,即让视频信息先展示到聊天框,再通过发送状态来标识当前的发送进度。

四、总结

富媒体发送在很多IM场景中均会涉及到,用什么样的技术实现能够让客服和用户之间沟通和交流更便捷是本文阐述的重点。通过在实际客服业务场景中的实践,本文的技术方案已经很好的解决了业务中的问题并且实际线上也一直比较稳定的在运行。从业务中发现问题,用技术手段解决问题,提升客服的解决效率,给用户带来好的体验是我们不断追求的目标,如果看了本文之后,你有更好的建议可以给我们留言。此外客服领域的技术点远不止这些,脚踏实地,一步一个脚印,相信即时通讯在客服领域的沉淀会越来越好。

五、知识扩展

1、文件读取的实现差异

URL.createObjectURL() 和FileReader.readAsDataURL(file)都可以取到文件的信息,为什么我们选择使用前者而非后者?

两者的主要区别在于:

  • 通过FileReader.readAsDataURL(file)获取到的是一段data:base64的字符串,base64位的字符串较大
  • 通过URL.createObjectURL(blob)获会创建一个DOMString,其中有包含了文件信息的URL(指定的 File 对象或 Blob 对象)

执行的时机的不同:

  • createObjectURL是立即的执行
  • FileReader.readAsDataURL是(过一段时间)异步执行

内存的使用不同:

  • createObjectURL返回一段带hash的url,并且一直存储在内存中,当document被触发了unload或者执行revokeObjectURL进行内存释放;
  • FileReader.readAsDataURL返回的是base64的字符串,比blob url消耗更多的内存,不过这个数据会通过垃圾回收机制自动清除。

使用选择:

  • 用createObjectURL能够节省性能,获取的速度也更快;
  • 如果设备性能足够好,而且想要获取图片的base64,可以用FileReader.readAsDataURL。

2、流媒体、富媒体、多媒体的概念

流媒体、富媒体、多媒体到底有什么区别?

流媒体:一边使用,后台一边下载后面可能要使用到的东西。
富媒体:文字、图片、视频、音频混排的页面内容。
多媒体:图片、文字、音频、视频等资料。其中流媒体是一种传输方式,富媒体是不同于纯文本的一种展示方式,多媒体是展示内容的一种手段。

推荐阅读:
得物客服IM消息通信SDK自研之路
微前端在客服域的实践

文|Jun

关注得物技术,做最潮技术人

富媒体在客服IM消息通信中的秒发实践相关推荐

  1. 客服系统对接微信公众号-访客在聊天界面扫码-临时访客绑定公众号OpenID可接收客服回复消息通知...

    访客在线咨询有以下几个问题: 1. 访客打开聊天窗口,一般情况下都是临时访客,只存在于当前浏览器. 2. 浏览器清理缓存,或者换一个浏览器,访客ID会重新生成就会变成一个新访客. 3. 访客咨询后,关 ...

  2. 一度智信:店铺客服如何正确处理中差评情况

    一度智信:店铺客服如何正确处理中差评情况 产品评价至关重要,一般来说评价会影响客户的购买,中差评会降低店铺整体信誉,想要减少中差评的产生,最关键的是产品本身和店铺的客服.如果商家自己的产品质量很有把握 ...

  3. 微信个人号多开云客服系统定制通信标准协议

    微信个人号多开云客服系统(SCRM客服系统)通信标准协议定义: 1.获取通信token请求消息(DeviceAuthReqMessage) 2.获取通信token结果消息(DeviceAuthRspM ...

  4. Java集成企业微信中的微信客服[接收消息和事件]

    目录 第一步登录企业微信后台管理界面 第二步设置接收事件服务器 第三步  先下载企业微信提供的解密工具包 PKCS7Encoder  对企业微信发送给企业后台的消息加解密示例代码 XMLParse  ...

  5. 闲鱼客服工具/消息管理系统:可以让客服放下手机,在电脑上管理闲鱼店铺的咨询信息

    闲鱼是一个很神奇的APP, 在上面几乎有一切二手的东西. 现在也有很多人在闲鱼发展自己的副业,甚至是事业,闲鱼客服的工作就成了整天面对手机,管理和回复商品的购买和咨询信息. 这款软件的目的是让闲鱼客服 ...

  6. PostgreSQL 无会话、有会话模式 - 客服平均响应速度(RT)实时计算实践(窗口查询\流计算)...

    标签 PostgreSQL , 无会话 , 客服响应速度 , 触发器 , rule , 窗口查询 背景 通常客服系统可能存在一对多,多对多的情况. 例如, 我们在使用淘宝时,与店家交流时,你根本不知道 ...

  7. Module Federation在客服工单业务中的最佳实践

    Module Federation:是模块联邦的意思,在webpack 5中流行起来的,也属于一种微前端方案. 一.背景 1.客服高频工作场景 一线客服: 基于一站式工作台中的在线工作台及电话工作台, ...

  8. 苹果系统引导修复工具_苹果客服渠道回应 iPhone 12 系列屏幕 “发绿”情况:将在新系统中修复...

    新闻转自IT之家 远洋 IT之家 11 月 17 日消息IT之家昨天曾报道,近日有用户反映 iPhone 12 出现屏幕发绿的问题,具体来说,iPhone 12 在深灰色状态下会出现屏幕周围泛绿的情况 ...

  9. java获取客服列表,java-从列表中获取处理案例的服务

    我有一些实现接口的处理程序.每个处理程序应根据一种或多种类型为某些情况提供服务.我通过自动连线获得了这些处理程序的列表. 因此,当我想在这些迭代器上使用这些处理程序并检查其中一些是否处理该情况时,如果 ...

最新文章

  1. 平面电子地图如何表现同一位置的POI
  2. LCD1602液晶显示模块的单片机驱动深入详解之硬件篇
  3. uboot更改gpio电平_ECBM系列教程4:单片机的手和脚——GPIO
  4. Runtime底层原理--动态方法解析、消息转发源码分析
  5. 笔记本电脑怎么清理灰尘_笔记本发热怎么办?这里有解决方案
  6. VC下ctreectrl的使用方法及节点前图标添加方法
  7. Caffe学习:pycaffe利用caffemodel进行分类=批处理
  8. 汇编语言编译器CmasmW的调试方法
  9. 用ArcGIS提取HWSD中的土壤单一属性数据
  10. 提高网站速度,分析工具page speed中文教程
  11. TOEFL wordlist 35
  12. 修改服务器bi,修改 FineBI 配置参数
  13. android实现跑马灯效果,Android用过TextView实现跑马灯效果的示例
  14. gif动态图如何制作?
  15. 面试题HTML +CSS
  16. 微信小程序 动态添加类名
  17. php buildtrees,thinkphp5带分组功能的树形结构的无限级分类节点展示
  18. 去天瑞地安面试了 这是我整个的面试流程
  19. YII2使用时出现The file or directory to be published does not exist
  20. ES集群宕机后处理——重新分配shards,负载均衡

热门文章

  1. CentOS LiveCD LiveDVD DVD 等版本的区别 以及 最新版7.1下载
  2. python copy deepcopy_python copy与deepcopy (拷贝与深拷贝)
  3. [2018.04.17][水][日志][6][#171~#181][贪心算法][已经丧心病狂][背景-amp;amp;amp;amp;gt;][最虚伪的算法]
  4. 送你一朵小红花,CSS实现一朵旋转的小红花
  5. Unity 协程开启、停止与生命周期
  6. 零基础-在window系统中通过VMware安装centos 7
  7. Git Cherry-pick (摘樱桃) 实现分支的部分提交合并到Master
  8. python pyc文件是啥_什么是.pyc文件
  9. 【面试题】Java语言有哪些优点
  10. Win10开发之UWP控件的隐藏空间