【微信小程序】小程序端录音、播放踩坑日记 - 新闻资讯 - 云南小程序开发|云南软件开发|云南网站建设-昆明葵宇信息科技有限公司

159-8711-8523

云南网建设/小程序开发/软件开发

知识

不管是网站,软件还是小程序,都要直接或间接能为您产生价值,我们在追求其视觉表现的同时,更侧重于功能的便捷,营销的便利,运营的高效,让网站成为营销工具,让软件能切实提升企业内部管理水平和效率。优秀的程序为后期升级提供便捷的支持!

您当前位置>首页 » 新闻资讯 » 小程序相关 >

【微信小程序】小程序端录音、播放踩坑日记

发表时间:2021-1-5

发布人:葵宇科技

浏览次数:121

一、前言
  • 开发背景:首次尝试小程序中实现录音、播放功能。
  • 开发框架:
    • taro 2.2.6
    • taro-ui 2.3.4
  • 难点描述:
    • 实现小程序录音、上传到后台
    • PC、IOS 和安卓端音频播放资源的地址,支持 mp3 下载链接

温馨提示:这篇文章重点介绍小程序的音频在各种环境录音和播放实践。适用对象:遇到小程序在 IOS 端无法播放音频的同学们和对小程序兼容性感兴趣的同学。

二、小程序录音、上传

2.1 注册事件监听

首先,介绍一下录音的部分。这里主要用到了小程序中的 wx.getRecorderManager() 模块部分。

直接放代码,感兴趣的可以去微信开发文档就了解下各种配置。

import Taro, { Component } from '@tarojs/taro'

export default class Index extends Component {
  ...
  // 声明录音管理器模块
  recorderManager = wx.getRecorderManager()

  componentDidMount() {
    // 抛出错误
    recorderManager.onError(() => {
      Taro.showToast({
        title: '录音失败!',
        duration: 1000,
        icon: 'none'
      })
    })
    // 录音结束时的处理
    recorderManager.onStop(res => {
      if (res.duration < 1000) {
        Taro.showToast({
          title: '录音时间太短',
          duration: 1000,
          icon: 'none'
        })
      } else {
        // content 是存储录音结束后的数据结构,用于调试
        this.setState({ content: res })
        wx.saveFile({
          tempFilePath: res.tempFilePath,
          success: result => {
            // 这里会调用一个文件上传的接口
            this.fileUpload(result.savedFilePath)
          }
        })
      }
    })
  }
  
  fileUpload(tempFilePath) {
    Taro.uploadFile({
      url: XXXApi,
      filePath: tempFilePath,
      name: 'file',
      header: {
        'content-type': 'multipart/form-data',
        cookie: Taro.getStorageSync('cookie') // 上传需要单独处理 cookie
      },
      formData: {
        method: 'POST' // 请求方式
      },
      success: res => {
        // 录音上传成功之后的处理
      }
    })
  }
}
复制代码

梳理一下:

  • componentDidMount 生命周期中,注册几个重要的事件。包括:监听录音错误事件监听录音结束事件
  • 在录音结束时,用 wx.savefile 将文件保存到本地
  • wx.savefile 成功的回调中,调用文件上传的接口,将文件上传到服务器。

2.2 实现录音事件处理函数

先看下 dom 节点部分:

<Text>上传语音</Text>
<Text
  onLongPress={this.handleRecordStart}
  onTouchend={this.handleRecordStop}
>
  长按说话
</Text>
复制代码

其中就两个事件:handleRecordStarthandleRecordStop。他们分别是长按时触发和手指松开时触发。

简单实现:

// longpress (长按)时触发
handleRecordStart(e) {
  this.setState({
    record: {
      // 修改录音数据结构,此时录音按钮样式会发生变化。
      text: '松开保存',
      type: 'recording'
    }
  })
  // 开始录音
  this.recorderManager.start({
    duration: 60000,
    sampleRate: 44100,
    numberOfChannels: 1,
    encodeBitRate: 192000,
    format: 'mp3',
    frameSize: 50
  }) 
  Taro.showToast({
    title: '正在录音',
    duration: 60000,
    icon: 'none'
  })
}

// touchend (手指松开)时触发
handleRecordStop() {
  // 复原在 start 方法中修改的录音的数据结构
  this.setState({
    record: {
      text: '长按录音',
      type: 'record'
    }
  })
  // 结束录音、隐藏 Toast 提示框
  wx.hideToast() 
  // 结束录音
  this.recorderManager.stop() 
}
复制代码

这里用了一个 record 对象来记录录音的状态。

注意 recorderManager.start 方法的参数中, duration 指录音时长,这里设置为 60000 msformat 值为 mp3,意思录音得到的音频文件为 mp3 格式。

温馨提示:最初开发没有设置成格式化为 mp3,导致后台同事增加了工作量(将 m4a 转换成 mp3),这里建议前端直接处理,很方便。


三、小程序端录音的播放

3.1 录音播放

说到音频播放,大家第一时间可能想到的是 Audio 标签,然后给其中的 src 属性动态赋值就好了。没错,PC 端确实是这样。但是小程序比较坑,如下图:

音频播放这里,我们选用了 wx.createInnerAudioContext() 接口。

温馨提示:如果音频上传到后台之后可以返回 .mp3 结尾的 url 链接(例如:http://47.104.167.164/faceVideo/result_2020_07_21_12_33_43.mp3),可以考虑直接利用 wx.createInnerAudioContext()play() 方法实现播放。

由于部分原因,我们后台上传音频文件后,返回的链接是一个云文件 ID(指浏览器打开可以下载此 mp3 文件)。而且经过测试发现,安卓端可以直接播放,IOS 端直接播放没有声音。

然后,请教了一下我们组的架构师,决定将文件先下载下来,然后保存到手机本地,最后播放(经过测试方案可行)。

我们直接看代码:

// 小程序音频播放 api
innerAudioContext = wx.createInnerAudioContext()

// 下载音频文件
downloadFile() {
  const FileSystemManager = wx.getFileSystemManager()
  const { voiceUrl } = this.state
  wx.downloadFile({
    url: voiceUrl,
    header: { 'Content-type': 'audio/mp3' },
    success: res => {
      // 只要服务器有响应数据,就会把响应内容写入文件并进入 success 回调,业务需要自行判断是否下载到了想要的内容
      if (res.statusCode === 200) {
        FileSystemManager.saveFile({
          tempFilePath: res.tempFilePath,
          // 文件地址为手机本地
          filePath: `${wx.env.USER_DATA_PATH}/${new Date().getTime()}.mp3`,
          success: result => {
            if (result.errMsg == 'saveFile:ok') {
              this.registerAudioContext(result.savedFilePath)
            }
          }
        })
      }
    }
  })
}

// 注册音频控件
registerAudioContext(path) {
  this.innerAudioContext.src = http://www.wxapp-union.com/path
  this.innerAudioContext.play()
  // 避开 IOS 端静音状态没法播放的问题
  this.innerAudioContext.obeyMuteSwitch = false
  this.innerAudioContext.onEnded(res => {
    // isPlaying 记录是否在播放中
    this.setState({ isPlaying: false })
    this.innerAudioContext.stop()
  })
  this.innerAudioContext.onError(res => {
    // 播放音频失败的回调
  })
  this.innerAudioContext.onPlay(res => {
    // 开始播放音频的回调
  })
  this.innerAudioContext.onStop(res => {
    // 播放音频停止的回调
  })
}
复制代码

这里做了两件事情:

  • wx.downloadFile() 接口将文件下载下来,注意参数中 header 属性, Content-type 值为 audio/mp3。即将此文件识别为音频类文件。这里用到微信里的文件管理器 wx.getFileSystemManager() ,接口中的 saveFile() 方法可以把文件保存到本地
  • wx.createInnerAudioContext()play() 方法播放存在本地的音乐 mp3 文件

3.2 性能优化

这里考虑到播放完之后,存在手机的录音文件会越来越多。我们想想办法,做一做性能优化工作。也就是在恰当的时机清楚多余文件。

代码如下:

componentWillUnmount() {
  this.clearDir()
}

// 删除下载的音频文件
clearDir() {
  const FileSystemManager = wx.getFileSystemManager()
  const __dirPath = wx.env.USER_DATA_PATH
  FileSystemManager.readdir({
    dirPath: __dirPath,
    success: res => {
      const { errMsg, files } = res
      if (errMsg == 'readdir:ok') {
        files.forEach(item => {
          FileSystemManager.unlink({
            filePath: `${__dirPath}/${item}`
          })
        })
      }
    }
  })
}
复制代码

梳理一下:

wx.getFileSystemManager() 接口中 readdir() 方法读取到指定目录(wx.env.USER_DATA_PATH)的所有文件。在其读取成功的回调中做一个 forEach 循环,然后用 unlink() 删除文件。最后将此方法放在生命周期 componentWillUnmount 中调用。


四、PC 端音频播放

小程序的录音和播放都简单的介绍了,这里也拓展一下。说一说 PC 端比较原始的音频播放方法。

项目中没有引用播放器插件,这里直接用 audio 标签来实现。 html 的部分如下:

const { voice_url, isPlaying } = this.state;

return (
  <>
    <p>
      <span>音频:</span>
      <Button onClick={this.onBtnClick}>{isPlaying ? '停止' : '播放'}</Button>
    </p>

    <audio
      id={`audio`}
      src={voice_url}
      autoPlay={true}
      ref={this.audioRef}
      preload={'auto'}
      onCanPlay={() => {}}
      onTimeUpdate={() => {}}>
      <track src={voice_url} kind='captions' />
    </audio>
  </>
)
复制代码

然后看下 PC 端解析播放部分,和小程序原理差不多,先下载,后播放。代码如下:

// 播放或者暂停
onBtnClick = () => {
  const { isPlaying } = this.state;
  // 区分播放还是暂停
  if (isPlaying) {
    this.audioRef.current.pause();
  } else {
    this.downloadFile();
  }
  this.setState({ isPlaying: !isPlaying });
};

// 下载文件
downloadFile = () => {
  const { download_url } = this.state;
  axios.get(download_url as string, { responseType: 'blob' }).then((res: any) => {
    const reader = new FileReader();
    const data = http://www.wxapp-union.com/res.data;
    reader.onload = e => {
      this.executeDownload(data);
    };
    reader.readAsText(data);
  });
};

// 在浏览器上预览音频文件
executeDownload = (data: any) => {
  if (!data) {
    return;
  }
  // 将文件转化音频流的链接
  const url = window.URL.createObjectURL(new Blob([data], { type: 'audio/mp3' }));
  // 前端存储这个链接
  this.setState({ voice_url: url });
};
复制代码

梳理:

  • 创建 audio 标签作为音频播放的容器
  • 点击页面的播放按钮触发文件下载方法
  • 通过 axios 下载资源文件,用 new FileReader() 读取文件,并且在文件完全加载时,利用 window.URL.createObjectURL() 方法生成可以在浏览器上预览音频文件的链接
  • audio 监听到 src 属性的变化时,会自动播放出声音

相关案例查看更多