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 相关文章推荐
如何使用 vue-cli 创建模板项目
Nov 19 Vue.js
vue动态合并单元格并添加小计合计功能示例
Nov 26 Vue.js
Vue $attrs &amp; inheritAttr实现button禁用效果案例
Dec 07 Vue.js
Vue实现随机验证码功能
Dec 29 Vue.js
vue中父子组件的参数传递和应用示例
Jan 04 Vue.js
vue二选一tab栏切换新做法实现
Jan 19 Vue.js
VUE实现吸底按钮
Mar 04 Vue.js
vue3.0中使用element的完整步骤
Mar 04 Vue.js
vue cli4中mockjs在dev环境和build环境的配置详情
Apr 06 Vue.js
Vue OpenLayer测距功能的实现
Apr 20 Vue.js
vue修饰符.capture和.self的区别
Apr 22 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操作XML、读取数据和写入数据的实现代码
2014/08/15 PHP
Centos下升级php5.2到php5.4全记录(编译安装)
2015/04/03 PHP
PHP生成json和xml类型接口数据格式
2015/05/17 PHP
Zend Framework教程之资源(Resources)用法实例详解
2016/03/14 PHP
phpcms实现验证码替换及phpcms实现全站搜索功能教程详解
2017/12/13 PHP
支持ie与FireFox的剪切板操作代码
2009/09/28 Javascript
javascript 基础篇3 类,回调函数,内置对象,事件处理
2012/03/14 Javascript
20分钟轻松创建自己的Bootstrap站点
2016/05/12 Javascript
jQuery使用经验小技巧(推荐)
2016/05/31 Javascript
解析jQueryEasyUI的使用
2016/11/22 Javascript
H5上传本地图片并预览功能
2017/05/08 Javascript
react中的ajax封装实例详解
2017/10/17 Javascript
JavaScript对JSON数组简单排序操作示例
2019/01/31 Javascript
vue2.0 实现富文本编辑器功能
2019/05/26 Javascript
基于JavaScript实现贪吃蛇游戏
2020/03/16 Javascript
js实现拾色器插件(ColorPicker)
2020/05/21 Javascript
[56:21]LGD vs IG 2018国际邀请赛小组赛BO2 第二场 8.18
2018/08/19 DOTA
python控制台中实现进度条功能
2015/11/10 Python
python 文件操作api(文件操作函数)
2016/08/28 Python
利用Python+Java调用Shell脚本时的死锁陷阱详解
2018/01/24 Python
Python Dataframe 指定多列去重、求差集的方法
2018/07/10 Python
python爬虫之urllib,伪装,超时设置,异常处理的方法
2018/12/19 Python
Python判断telnet通不通的实例
2019/01/26 Python
Python中函数参数匹配模型详解
2019/06/09 Python
Django REST Framework之频率限制的使用
2019/09/29 Python
使用python制作游戏下载进度条的代码(程序说明见注释)
2019/10/24 Python
Tensorflow 实现将图像与标签数据转化为tfRecord文件
2020/02/17 Python
django实现日志按日期分割
2020/05/21 Python
python使用hdfs3模块对hdfs进行操作详解
2020/06/06 Python
Python Selenium模块安装使用教程详解
2020/07/09 Python
德国柯吉澳趣味家居:Koziol
2017/08/24 全球购物
亚洲独特体验旅游专家:eOasia
2018/08/15 全球购物
澳大利亚优质葡萄酒专家:Vintage Cellars
2019/01/08 全球购物
企业文化标语口号
2014/06/09 职场文书
公司领导班子民主生活会对照检查材料
2014/10/02 职场文书
vue @click.native 绑定原生点击事件
2022/04/22 Vue.js