关于前端:富媒体在客服IM消息通信中的秒发实践

8次阅读

共计 6525 个字符,预计需要花费 17 分钟才能阅读完成。

引言

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

一、背景

客服一站式平台旨在为得物生态内的客服域服务人员提供一站式的服务办公平台。咱们有多条业务线,客服在和用户聊天的过程中,有很多场景须要发送富媒体。跟一般的文本传输相比,富媒体能够直观的让用户理解到音讯内容,然而在传输过程中也面临着文件大、内存耗费大、传输过程漫长等问题。

二、面临的挑战

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

首先通过文件上传服务上传到 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.result
        if (/video/g.test(file.type)) {const video = document.createElement('video')
          video.muted = true
          video.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 作为预览视频的 url
    state.previewVideoSrc = URL.createObjectURL(file)
    state.previewVideo = true
    state.cachePreviewVideoFile = file
    nextTick(() => {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.result
        if (/video/g.test(file.type)) {const video = document.createElement('video')
          video.muted = true
          video.setAttribute('autoplay', 'autoplay')
          // target 只作为 url 创立视频用于获取视频大小、播放时长等根本信息,不用于页面渲染
          video.setAttribute('src', target)
          video.addEventListener('loadeddata', () => {const canvas = document.createElement('canvas')
            canvas.width = video.videoWidth
            canvas.height = video.videoHeight
            const width = video.videoWidth
            const height = video.videoHeight
            canvas.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 : urlSuffix
  if (!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 = true
      video.setAttribute('autoplay', 'autoplay')
      // blob 作为 url: URL.createObjectURL(file)
      video.setAttribute('src', blob)
      video.addEventListener('loadeddata', () => {
        const width = video.videoWidth
        const height = video.videoHeight
        resolve({
          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
 @得物技术公众号

正文完
 0