Vue +WebSocket + WaveSurferJS 实现H5聊天对话交互的实例


Posted in Vue.js onNovember 18, 2020

引言

在与实现了语音合成、语义分析、机器翻译等算法的后端交互时,页面可以设计成更为人性化、亲切的方式。我们采用类似于聊天对话的实现,效果如下:

  • 智能客服(输入文本,返回引擎处理后的文本结果)

Vue +WebSocket + WaveSurferJS 实现H5聊天对话交互的实例

  • 语音合成(输入文本,返回文本以及合成的音频)

Vue +WebSocket + WaveSurferJS 实现H5聊天对话交互的实例

如上图所示,返回文本后,再返回合成出的音频。
音频按钮嵌在对话气泡中,可以点击播放。

  • 语音识别(在页面录制语音发送,页面实时展示识别出的文本结果)

Vue +WebSocket + WaveSurferJS 实现H5聊天对话交互的实例

实现功能及技术要点

1、基于WebSocket实现对话流
页面与后端的交互是实时互动的,所以采用WebSocket协议,而不是HTTP请求,这样后端推送回的消息可以实时显示在页面上。
WebSocket的返回是队列的、无序的,在后续处理中我们也需要注意这一点,在后文中会说到。
2、调用设备麦克风进行音频录制和转码加头,基于WebAudio、WaveSurferJS等实现音频处理和绘制
3、基于Vue的响应式页面实现
4、CSS3 + Canvas + JS 交互效果优化

  • 录制音频CSS动画效果
  • 聊天记录自动滚动

下面给出部分实现代码。

集成WebSocket

我们的聊天组件是页面侧边打开的抽屉(el-drawer),Vue组件会在打开时创建,关闭时销毁。在组件中引入WebSocket,并管理它的开、关、消息接收和发送,使它的生命周期与组件一致(打开窗口时创建ws连接,关闭窗口时关闭连接,避免与后台连接过多。)

created(){
 if (typeof WebSocket === 'undefined') {
  alert('您的浏览器不支持socket')
 } else {
  // 实例化socket
  this.socket = new WebSocket(this.socketServerPath)
  // 监听socket连接
  this.socket.onopen = this.open
  // 监听socket错误信息
  this.socket.onerror = this.error
  // 监听socket消息
  this.socket.onmessage = this.onMessage
  this.socket.onclose = this.close
 }
}
destroyed(){
 this.socket.close()
}

如上,将WebSocket的事件绑定到JS方法中,可以在对应方法中实现对数据的接收和发送。
打开浏览器控制台,选中指定的标签,便于对WebSocket连接进行监控和查看。

Vue +WebSocket + WaveSurferJS 实现H5聊天对话交互的实例

音频录制采集

从浏览器端音频和视频采集基于网页即时通信(Web Real-Time
Communication,简称WebRTC) 的API。通过WebRTC的getUserMedia实现,获取一个MediaStream对象,将该对象关联到AudioContext即可获得音频。

可参考RecorderJS的实现: https://github.com/mattdiamond/Recorderjs/blob/master/examples/example_simple_exportwav.html

if (navigator.getUserMedia) {
  navigator.getUserMedia(
  { audio: true }, // 只启用音频
  function(stream) {
   var context = new(window.webkitAudioContext || window.AudioContext)()
   var audioInput = context.createMediaStreamSource(stream)
   var recorder = new Recorder(audioInput)

  },
  function(error) {
   switch (error.code || error.name) {
   case 'PERMISSION_DENIED':
   case 'PermissionDeniedError':
    throwError('用户拒绝提供信息。')
    break
   case 'NOT_SUPPORTED_ERROR':
   case 'NotSupportedError':
    throwError('浏览器不支持硬件设备。')
    break
   case 'MANDATORY_UNSATISFIED_ERROR':
   case 'MandatoryUnsatisfiedError':
    throwError('无法发现指定的硬件设备。')
    break
   default:
    throwError('无法打开麦克风。异常信息:' + (error.code || error.name))
    break
   }
  }
  )
 } else {
  throwError('当前浏览器不支持录音功能。')
 }

注意: 若navigator.getUserMedia获取到的是undefined,是Chrome浏览器的安全策略导致的,需要通过https请求或配置浏览器,配置地址: chrome://flags/#unsafely-treat-insecure-origin-as-secure

浏览器采集到的音频为PCM格式(PCM (脉冲编码调制 Pulse Code Modulation)),需要对音频加头才能在页面上进行播放。注意加头时采样率、采样频率、声道数量等必须与采样时相同,不然加完头后的音频无法解码。参考查看https://github.com/mattdiamond/Recorderjs/blob/master/src/recorder.js中exportWav方法。

业务中对接的语音识别引擎为实时转写引擎,即:不是录制完成后再发送,而是一边录制一边进行编码并发送。
使用onaudioprocess方法监听语音的输入:

Vue +WebSocket + WaveSurferJS 实现H5聊天对话交互的实例

参考这个实现,我们可以在每次监听到有数据写入时,从buffer中获取到录制到的数据,并进行编码、压缩,再通过WebSocket发送。

Vue组件设计和业务实现

分析页面业务逻辑,将代码拆分成两个组件:
ChatDialog.vue 聊天对话框页面,根据输入类型,分为文本输入、语音输入。
ChatRecord.vue聊天记录组件,根据发送方(自己或者系统)展示向左/向右的气泡,根据内容显示文本、音频等。ChatDialog是ChatRecord的父组件,遍历ChatDialog中的chatList对象(Array),将chatList中的项注入到ChatRecord中。

<div class="chat-list">
   <div v-for="(item,index) in chatList" :key="index" class="msg-wrapper">
    <chat-record ref="chatRecord" :data="item" @showJson="showJsonDialog"></chat-record>
   </div>
   <div id="msg_end" style="height:0px; overflow:hidden"></div>
  </div>
</div>

对于聊天记录的气泡展示,与数据类型相关性很强,ChatRecord组件只关心对数据的处理和展示,我们可以完全不用关心消息的发送、接收、音频的录制、停止录制、接受音频等逻辑,只需要根据数据来展示不同的样式即可。
这样Vue的响应式就充分获得了用武之地:无需用代码对样式展示进行控制,只需要设计合理的数据格式和样式模板,然后注入不同的数据即可。
模板页面: 使用v-if控制,修改chatList里的对象内容即可改变页面展示。

根据业务需求,将ChatRecord可能接收到的数据分为以下几类:

发送方为自己:

  • 文本输入,显示文本

实现简单,不做赘述。

  • 语音输入 Loading状态,显示波纹动画和计时

Vue +WebSocket + WaveSurferJS 实现H5聊天对话交互的实例

该动画使用CSS实现,参考地址: https://www.cnblogs.com/lhb25/p/loading-spinners-animated-with-css3.html

计时器使用JS的setInterval方法,每100ms更新一次录制时长

this.recordTimer = setInterval(() => {
  this.audioDuration = this.audioDuration + 0.1
  }, 100)

停止后清空计时器:

  • 语音输入完毕,根据录制的语音,绘制波纹

效果:

Vue +WebSocket + WaveSurferJS 实现H5聊天对话交互的实例

使用wavesurfer插件:

initWaveSurfer() {
  this.$nextTick(() => {
  this.wavesurfer = WaveSurfer.create({
   container: this.$refs.waveform,
   height: 20,
   waveColor: '#3d6fff',
   progressColor: 'blue',
   backend: 'MediaElement',
   mediaControls: false,
   audioRate: '1',
   fillParent: false,
   maxCanvasWidth: 500,
   barWidth: 1,
   barGap: 2,
   barHeight: 5,
   barMinHeight: 3,
   normalize: true,
   cursorColor: '#409EFF'
  })
  this.convertAudioToUrl(this.waveAudio).then((res) => {
   this.wavesurfer.load(res)

   setTimeout(() => {
   this.audioDuration = this.getAudioDuration()
   }, 100)
  })
  })
 },

 // 将音频转化成url地址
 convertAudioToUrl(audio) {
  let blobUrl = ''
  if (this.data.sendBy === 'self') {
  blobUrl = window.URL.createObjectURL(audio)
  return new Promise((resolve) => {
   resolve(blobUrl)
  })
  } else {
  return this.base64ToBlob({
   b64data: audio,
   contentType: 'audio/wav'
  })
  }
 },

 base64ToBlob({ b64data = '', contentType = '', sliceSize = 512 } = {}) {
  return new Promise((resolve, reject) => {
  // 使用 atob() 方法将数据解码
  let byteCharacters = atob(b64data)
  let byteArrays = []
  for (
   let offset = 0;
   offset < byteCharacters.length;
   offset += sliceSize
  ) {
   let slice = byteCharacters.slice(offset, offset + sliceSize)
   let byteNumbers = []
   for (let i = 0; i < slice.length; i++) {
   byteNumbers.push(slice.charCodeAt(i))
   }
   // 8 位无符号整数值的类型化数组。内容将初始化为 0。
   // 如果无法分配请求数目的字节,则将引发异常。
   byteArrays.push(new Uint8Array(byteNumbers))
  }
  let result = new Blob(byteArrays, {
   type: contentType
  })
  result = Object.assign(result, {
   // 这里一定要处理一下 URL.createObjectURL
   preview: URL.createObjectURL(result),
   name: `XXX.wav`
  })
  resolve(window.URL.createObjectURL(result))
  })
 },

发送方为系统:

  • 仅返回文本:显示文本
  • 仅返回音频(参考发送方为自己的实现)

Vue +WebSocket + WaveSurferJS 实现H5聊天对话交互的实例

  • 返回文本,随即返回文本对应的合成音频,显示文本和播放按钮

Vue +WebSocket + WaveSurferJS 实现H5聊天对话交互的实例

页面嵌入audio标签,将hidden设置为true使其不显示:

<div class="audio-player">
   <svg-icon v-if="!isPlaying" icon-class='play' @click="onClickAudioPlayer" />
   <svg-icon v-else icon-class='pause' @click="onClickAudioPlayer" />
   <audio :src="playAudioUrl" autostart="true" hidden="true" ref="audioPlayer" />
  </div>

playAudioUrl的生成参考上面生成的wavesurfer的url。
使用isPlaying参数记录当前音频的播放状态,并使用setTimeout方法,当播放了音频时长后,将播放按钮自动置为play

onClickAudioPlayer() {
  if (this.isPlaying) {
  this.$refs.audioPlayer.pause()
  this.isPlaying = false
  } else {
  // 每次点击时,开始播放,并在播放完毕将isPlaying置为false
  this.$refs.audioPlayer.currentTime = 0
  this.$refs.audioPlayer.play()
  this.isPlaying = true

  setTimeout(() => {
   // 将正在播放重置为false
   this.isPlaying = false
  }, Math.ceil(this.$refs.audioPlayer.duration) * 1000)
  }
 },

聊天记录自动定位到最后一条:

  • 使用scrollIntoView()方法

记录每次会话对应的记录ID(recordId):

  • 定义单次会话的id,并在返回的消息中回传,从而建立多条websocket返回的关联关系。

以上就是全部实现。难点主要是请求麦克风权限和对音频进行编码,在加wav头时必须保证和采样时的采样率、频率一致 。

以上就是Vue +WebSocket + WaveSurferJS 实现H5聊天对话交互的实例的详细内容,更多关于vue 实现H5聊天对话交互的资料请关注三水点靠木其它相关文章!

Vue.js 相关文章推荐
vuex的数据渲染与修改浅析
Nov 26 Vue.js
Vue实现图书管理小案例
Dec 03 Vue.js
vue-router定义元信息meta操作
Dec 07 Vue.js
vue图片裁剪插件vue-cropper使用方法详解
Dec 16 Vue.js
Vue——前端生成二维码的示例
Dec 19 Vue.js
Vue 修改网站图标的方法
Dec 31 Vue.js
vue实现防抖的实例代码
Jan 11 Vue.js
vue实现拖拽进度条
Mar 01 Vue.js
Vue实现导入Excel功能步骤详解
Jul 03 Vue.js
详解Vue slot插槽
Nov 20 Vue.js
vue使用echarts实现折线图
Mar 21 Vue.js
vue+iview实现手机号分段输入框
Mar 25 Vue.js
springboot+vue实现文件上传下载
Nov 17 #Vue.js
vue 表单输入框不支持focus及blur事件的解决方案
Nov 17 #Vue.js
解决vue elementUI 使用el-select 时 change事件的触发问题
Nov 17 #Vue.js
Vue项目利用axios请求接口下载excel
Nov 17 #Vue.js
vue实现下载文件流完整前后端代码
Nov 17 #Vue.js
vue+iview实现文件上传
Nov 17 #Vue.js
vue中echarts的用法及与elementui-select的协同绑定操作
Nov 17 #Vue.js
You might like
PHP伪造referer实例代码
2008/09/20 PHP
php的urlencode()URL编码函数浅析
2011/08/09 PHP
PHP图片裁剪函数(保持图像不变形)
2014/05/04 PHP
php获取网页中图片、DIV内容的简单方法
2014/06/19 PHP
详谈PHP文件目录基础操作
2014/11/11 PHP
js获取height和width的方法说明
2013/01/06 Javascript
Extjs4.0设置Ext.data.Store传参的请求方式(默认为GET)
2013/04/02 Javascript
jQuery学习笔记(1)--用jQuery实现异步通信(用json传值)具体思路
2013/04/08 Javascript
node.js中的buffer.fill方法使用说明
2014/12/14 Javascript
JS基于cookie实现来宾统计记录访客信息的方法
2015/08/04 Javascript
基于JavaScript实现通用tab选项卡(通用性强)
2016/01/07 Javascript
JavaScript判断数字是否为质数的方法汇总
2016/06/02 Javascript
jQuery bt气泡实现悬停显示及移开隐藏功能的方法
2016/07/12 Javascript
浅谈Javascript中的Label语句
2016/12/14 Javascript
Bootstrap DateTime Picker日历控件简单应用
2017/03/25 Javascript
jQuery遮罩层实例讲解
2017/05/11 jQuery
微信小程序中换行空格(多个空格)写法详解
2018/07/10 Javascript
Vue2.0生命周期的理解
2018/08/20 Javascript
jQuery选择器选中最后一个元素,倒数第二个元素操作示例
2018/12/10 jQuery
微信小程序数据统计和错误统计的实现方法
2019/06/26 Javascript
layer.open的自适应及居中及子页面标题的修改方法
2019/09/05 Javascript
Python+MongoDB自增键值的简单实现
2016/11/04 Python
Selenium(Python web测试工具)基本用法详解
2018/08/10 Python
Python学习笔记之Zip和Enumerate用法实例分析
2019/08/14 Python
Python 异常处理Ⅳ过程图解
2019/10/18 Python
Pycharm配置PyQt5环境的教程
2020/04/02 Python
keras中的卷积层&amp;池化层的用法
2020/05/22 Python
python中time、datetime模块的使用
2020/12/14 Python
HTML5 本地存储和内容按需加载的思路和方法
2011/04/07 HTML / CSS
Aosom西班牙:家具在线商店
2020/06/11 全球购物
大学毕业生通用自荐信范文
2013/10/31 职场文书
计生专干事迹
2014/05/28 职场文书
搞笑的爱情检讨书
2014/10/01 职场文书
2016年第29个世界无烟日宣传活动总结
2016/04/06 职场文书
redis cluster支持pipeline的实现思路
2021/06/23 Redis
MySQL中order by的执行过程
2022/06/05 MySQL