微信小程序流式录音RecorderManager.onFrameRecorded
录音的API有那些?什么叫流式 & 非流式?
首先来说,在微信小程序的中有很多的有关录音的API,了解这些API的特性,有助于我们在不同的产品需求下做技术选型。微信的录音相关的api主要在这里: https://developers.weixin.qq.com/miniprogram/dev/api/media/recorder/wx.stopRecord.html ,表面上看主要是几个大的分类:
-
wx.stopRecord(Object object) // 停止录音
-
wx.startRecord(Object object) // 开始录音
-
wx.getRecorderManager() // 获取全局唯一的录音管理器 RecorderManager
- RecorderManager // 全局唯一的录音管理器 RecorderManager
前两个自然不用说了,代表的是第一个时代的录音接口,注意的是从基础库 1.6.0 开始,本接口停止维护(推荐使用后面的),后两个是配套的,通过调用wx.getRecorderManager() 获取全局唯一的录音管理器 RecorderManager,RecorderManager是一个单实例,他能提供如下方法:
RecorderManager.start(Object object)
开始录音
RecorderManager.pause()
暂停录音
RecorderManager.resume()
继续录音
RecorderManager.stop()
停止录音
RecorderManager.onStart(function callback)
监听录音开始事件
RecorderManager.onResume(function callback)
监听录音继续事件
RecorderManager.onPause(function callback)
监听录音暂停事件
RecorderManager.onStop(function callback)
监听录音结束事件
RecorderManager.onFrameRecorded(function callback)
监听已录制完指定帧大小的文件事件。如果设置了 frameSize,则会回调此事件。
RecorderManager.onError(function callback)
监听录音错误事件
RecorderManager.onInterruptionBegin(function callback)
监听录音因为受到系统占用而被中断开始事件。以下场景会触发此事件:微信语音聊天、微信视频聊天。此事件触发后,录音会被暂停。pause 事件在此事件后触发
RecorderManager.onInterruptionEnd(function callback)
监听录音中断结束事件。在收到 interruptionBegin 事件之后,小程序内所有录音会暂停,收到此事件之后才可再次录音成功。
这些api,都是RecorderManager实例对象的方法,on开头的都是事件回调函数,其他的就是对RecorderManager的操作方法,那么流式和非流式如何体现呢?首先说一下二者的本质区别:
- 非流式:也可以理解为整包上传,即有明确的开始和结束,也就是一个完整的音频文件,我们要等音频结束后在上传或处理文件。这样做的好处在于实现成本低,后台接收到完整文件后就可以直接处理了。前端交互页比较好处理。但是一旦录音时间比较长,文件就很大,上传时就有可能超过请求报文的大小限制,后台接收了大文件,处理时间页会比较长,会感觉接口响应超时,或比较慢,不适合实时性要求较高的场景。但较短的音频比较适合非流式
- 流式:是不等音频结束,一旦采集音频满足一定的帧大小后就返回这一段的音频(具体每一段是不是完整文件,要看系统平台)。我们在监听函数中拿到这一段的音频,就马上上传或处理-》得到处理结果,马上呈现到页面上,这样一边采集,一边处理,一边呈现结果。实时效果比较好。后台每次接受的文件大小也不会太大,处理速度也比较有保证。但是一方面前端交互比之前复杂,另一方面后台逻辑要能够处理排序逻辑(保证接收的顺序)。比较适合较长音频的场景。
两种方式,没有好坏之分,只有使用场景的不同,需要结合自身业务选择,下面来讲一下如何用微信的api来实现非流式和流式录音。
非流式如何实现录音功能
既然是获取一段完整的音频文件,那么就需要有明确的开始和结束,可以参考这种写法:
1 wx.startRecord({ 2 success (res) { 3 const tempFilePath = res.tempFilePath 4 } 5 }) 6 setTimeout(function () { 7 wx.stopRecord() // 结束录音 8 }, 10000)
注意2点,这里返回的是startRecord 的回调函数;返回的是一个临时文件路径。如果想上传到后台
处理,还需要调用微信的上传文件的接口:wx.uploadFile(Object object)
1 wx.startRecord({ 2 success (res) { 3 const tempFilePath = res.tempFilePath 4 wx.uploadFile({ 5 url: \'https://example.weixin.qq.com/upload\', //仅为示例,非真实的接口地址 6 filePath: tempFilePaths[0], 7 name: \'file\', 8 formData: { 9 \'user\': \'test\' 10 }, 11 success (res){ 12 const data = res.data 13 //do something 14 } 15 }) 16 } 17 }) 18 setTimeout(function () { 19 wx.stopRecord() // 结束录音 20 }, 10000)
如果你使用新版的接口(RecorderManager)的话,音频文件的获取:并不是在“start”里面而是“end”:
RecorderManager.onStop(function callback)
监听录音结束事件
参数
function callback
录音结束事件的回调函数
参数
Object res
属性 | 类型 | 说明 |
---|---|---|
tempFilePath | string | 录音文件的临时路径 |
duration | number | 录音总时长,单位:ms |
fileSize | number | 录音文件大小,单位:Byte |
也就是说文件是在回调函数的tempFilePath参数中,其他的也都同理。
流式如何实现录音功能
由于音频的获取是不断采集,每采集够一定音频帧数,就可以进行音频处理,所以音频的数据回调并不依赖明确的停止事件,而是满足这几点:
- 在RecorderManager.start时配置好frameSize(也就是每一个分片的帧大小)指定帧大小,单位 KB。传入 frameSize 后,每录制指定帧大小的内容后,会回调录制的文件内容,不指定则不会回调。暂仅支持 mp3 格式。
- 因此format也要设置为mp3
- 按照你和后台的约定,把采样率、录音通道数、编码码率设置好
- 最后在 RecorderManager.onFrameRecorded(function callback) 的回调中获取数据frameBuffer 和 是否为最后一帧isLastFrame 也就是https://developers.weixin.qq.com/miniprogram/dev/api/media/recorder/RecorderManager.onFrameRecorded.html
RecorderManager.onFrameRecorded(function callback)
监听已录制完指定帧大小的文件事件。如果设置了 frameSize,则会回调此事件。
参数
function callback
已录制完指定帧大小的文件事件的回调函数
参数
Object res
属性 | 类型 | 说明 |
---|---|---|
frameBuffer | ArrayBuffer | 录音分片数据 |
isLastFrame | boolean | 当前帧是否正常录音结束前的最后一帧 |
1 let seq = 0 2 recorderManager.onFrameRecorded(res => { 3 const {frameBuffer, isLastFrame} = res 4 const base64Buffer = wepy.arrayBufferToBase64(frameBuffer) 5 let url 6 const data = { 7 miniappname: \'fanyijun\' 8 } 9 url = `${你的域名}/recorder/xxxxxx` 10 data.isEnd = isLastFrame ? 1 : 0 11 data.lang = recordObj.lang 12 data.sessionUuid = recordObj.sessionUuid 13 data.audio = base64Buffer 14 data.seq = seq 15 // console.log(\'recorderFrame url: \', url) 16 console.log(\'recorderFrame data: \', data) 17 wepy.request({ 18 url, 19 data, 20 method: \'POST\' 21 }).then(res => { 22 // 处理逻辑 23 }).catch(e => { 24 console.log(\'err:\', e) 25 }) 26 seq++ 27 })
这样,我们就将每次监听到的音频分片都上传到服务器了 ,注意流式接口返回的不是音频文件的临时地址,而是frameBuffer,因为他不是完整的文件,微信本地也不会存储这个文件。
两种方式的区别在哪里
调用风格 | 文件大小 | 返回值 | 音频格式 | |
非流式录音接口 | stop后在回调中拿到完整的文件 | 根据实际录音时长(start和end的间隔)决定 | 临时文件本地地址,徐调用上传接口 | mp3、aac、silk |
流式录音接口 | 在onFrameRecordedzh中不断获取分片, 持续获取,持续处理 |
根据设定分片大小决定 | frameBuffer,可普通请求上传 | mp3 |
非流式的分片到底是什么?
刚刚说到了分片,那么分片到底是什么,是不是独立的文件,能不能单独处理?我们一起来看一下:
首先我在内网咨询了小程序这边的同事,得到的反馈是流式分片返回的是frameBuffer,不能独立使用。我同时也自己做了实验验证了一下。那就是那每一片frameBuffer,拿出来,分别写入文件查看,因为Buffer是一种二进制数据类型,我们直接在node中就可以保存文件,因此我将一次录音的相邻的音频分片分别写入:
1 var fs = require(\'fs\'); 2 var file = require(\'./file\'); 3 4 var data = file.data; 5 6 var now = Date.now(); //获取系统当前时间数值 7 var savePath = \'./\' + now + \'.mp3\'; //服务器存储文件名 8 let dataBuffer = new Buffer(data, \'base64\'); 9 console.log(`data.length ${data.length}`); 10 console.log(`dataBuffer: leng ${dataBuffer.byteLength}`); 11 12 fs.writeFile(savePath, aaa, function(err) { 13 if (err) { 14 console.log(err); 15 } 16 });
发现每个文件都可以独立的播放,那么我们来试一下把这些frameBuffer链接在一起呢:
1 var fs = require(\'fs\'); 2 var file = require(\'./file\'); 3 4 var datas = file.datas; 5 6 var now = Date.now(); //获取系统当前时间数值 7 var savePath = \'./\' + now + \'.mp3\'; //服务器存储文件名 8 // let dataBuffer = new Buffer(data, \'base64\'); 9 // console.log(`data.length ${data.length}`); 10 // console.log(`dataBuffer: leng ${dataBuffer.byteLength}`); 11 12 let aaa = Buffer.concat( 13 datas.map(x => { 14 return new Buffer(x, \'base64\'); 15 }) 16 ); 17 18 fs.writeFile(savePath, aaa, function(err) { 19 if (err) { 20 console.log(err); 21 } 22 });
发现这个链接frameBuffer后的音频播放出来就是我说的内容,文件大小也符合。但是为什么不建议后台直接使用呢?
“分片分别转码在合并” vs 合并后再转码的MD5,并不相同(文件大小和长度相等),因此不能看作是同一音频,后台同事和我的解释就是会引入噪声,目前来看仍然需要之前说的排序方案。
请发表评论