【微信小程序】小程序端录音、播放指南

文章首发:掘金主页

一、前言

  • 开发背景:首次尝试小程序中实现录音、播放功能。
  • 开发框架:
    • 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 端确实是这样。但是小程序比较坑,如下图:

image

音频播放这里,我们选用了 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 = 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 = 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 属性的变化时,会自动播放出声音

五、感谢

  • 如果本文对你有帮助,就点个赞支持下吧!感谢阅读。
  • 文中还有很多不完善的地方,欢迎大家在评论区提出疑问。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容