随着公司产品的业务扩展,今年算是和浏览器的录音功能硬磕上了。遇到了很多奇葩的问题以及一些更多的扩展吧~这里记录一下分享给一样遇到问题后脑袋疼的各位。php
这个场景仍是存在的。在websocket和server的交互上可能不存在问题。可是若是是原生应用间的交互,为了保证数据的一致性,只传string的状况下就须要用到了。前端
解析base64变为arrayBuffer.vue
function base642ArrayBuffer() {
const binary_string = window.atob(base64); // 解析base64
const len = binary_string.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
// 若是不`.buffer`则返回的是Unit8Array、各有各的用处吧
// Unit8Array能够用来作fill(0)静音操做,而buffer不行
return bytes.buffer;
}
复制代码
因为浏览器不能支持播放pcm数据,因此若是后端server”不方便“给你加上wav
请求头.那咱们须要本身造一个wav的头(也就是那44个字节)ios
function buildWaveHeader(opts) {
const numFrames = opts.numFrames;
const numChannels = opts.numChannels || 1;
const sampleRate = opts.sampleRate || 16000; // 采样率16000
const bytesPerSample = opts.bytesPerSample || 2; // 位深2个字节
const blockAlign = numChannels * bytesPerSample;
const byteRate = sampleRate * blockAlign;
const dataSize = numFrames * blockAlign;
const buffer = new ArrayBuffer(44);
const dv = new DataView(buffer);
let p = 0;
p = this.writeString('RIFF', dv, p); // ChunkID
p = this.writeUint32(dataSize + 36, dv, p); // ChunkSize
p = this.writeString('WAVE', dv, p); // Format
p = this.writeString('fmt ', dv, p); // Subchunk1ID
p = this.writeUint32(16, dv, p); // Subchunk1Size
p = this.writeUint16(1, dv, p); // AudioFormat
p = this.writeUint16(numChannels, dv, p); // NumChannels
p = this.writeUint32(sampleRate, dv, p); // SampleRate
p = this.writeUint32(byteRate, dv, p); // ByteRate
p = this.writeUint16(blockAlign, dv, p); // BlockAlign
p = this.writeUint16(bytesPerSample * 8, dv, p); // BitsPerSample
p = this.writeString('data', dv, p); // Subchunk2ID
p = this.writeUint32(dataSize, dv, p); // Subchunk2Size
return buffer;
}
function writeString(s, dv, p) {
for (let i = 0; i < s.length; i++) {
dv.setUint8(p + i, s.charCodeAt(i));
}
p += s.length;
return p;
}
function writeUint32(d, dv, p) {
dv.setUint32(p, d, true);
p += 4;
return p;
}
function writeUint16(d, dv, p) {
dv.setUint16(p, d, true);
p += 2;
return p;
}
复制代码
把头和pcm进行一次拼装git
concatenate(header, pcmTTS);
function concatenate(buffer1, buffer2) {
const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
tmp.set(new Uint8Array(buffer1), 0);
tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
return tmp.buffer;
}
复制代码
转成可播放buffer流,能够用来获取时间,若是是多段pcm数据流还能够进行组装拼接github
audioCtx.decodeAudioData(TTS, (buffer) => { 存储起来准备播放 });
// buffer.duration 能够用来判断播放时长
// buffer
复制代码
播放web
const source = audioCtx.createBufferSource();
const gainNode = audioCtx.createGain();
source.buffer = buffer;
gainNode.gain.setTargetAtTime(0.1, audioCtx.currentTime + 2, 5);
source.connect(gainNode);
gainNode.connect(context.destination);
source.start('须要播音的时长 简单的能够用buffer.duration或者本身计算拼接后的长度逻辑' + this.context.currentTime);// 这里必需要加上currentTime
复制代码
在业务需求中,有一个比较坑的需求。咱们产品的场景是模拟一个机器人和用户的通信对话过程。中间涉及机器人音频播放和用户的说话录音(由于功能上的需求,要求机器人播音时仍然录音来保证抢话逻辑的存在)。原本这个方案在佩戴耳机的场景下仍然可以作到表现还不错,可是在一次定制化需求下要求实现以上功能的状况下手机音频公放,不准佩戴耳机。而后咱们就崩溃了,花了不少时间去调研实现(其实这块没前端啥事,可是能够整理一下个人认知)。算法
由于各类缘由吧,咱们开始调试怎么原生和网页交互(原生应用负责采集音频,音频的pcm流经过方法回调提供给到网页中进行后续的处理)。这算是我第一次对接原生应用,再加上我司目前还不须要这方面开发人员,因此可能踩了一些在你们看来常识性的问题。也稍做整理:chrome
之前一直觉得交互形式是带有回调函数等花里胡哨的操做的。对接上后才知道,两端的调用都只能用简单的方法调用传参。这就致使一个问题,咱们和原生应用的交互须要把方法绑定在window
下,而绑定在window
下的方法没有拥有vue的this
上下文,因此为了打通原生应用的pcm数据流能正常下发到vue实例中进行逻辑处理,我写了一个简单的事件订阅者模式
,经过订阅、通知的形式来实现了。后端
在嵌入webview以后最大的问题大概就是咱们要怎么看chrome的日志了。可能如今采用的方式仍是一个比较蛋疼的实现方式,我分别下载了IOS的开发工具和安卓的开发工具,而后让他们帮忙把环境给我搭起来,以后调整就是我本身的事情了。这样的方法有个好处,就是假如我遇到一些小问题(涉及原生的改动),我能够直接本身查一下上手改一些小逻辑,不须要依赖别人,提升必定的效率。
这里还有个坑,可是就是安卓开启了chrome的调试模式后,打开控制台会出现404报错。其实这须要你用魔法上网以后才能正常访问。否则无论你咋捣鼓都不会成功滴。
webview嵌入原生应用后有不少权限上的问题,例如是否容许localstorage、是否容许非法的安全证书(本地开发会伪造证书来模拟https)、是否容许开启录音权限、https是否容许加载http资源、甚至细致到播音等等。遇到这个问题个人办法是,尽量和搭档描述清楚个人页面会作什么操做,而后由他们去判断给你开什么权限
IOS的坑实在太多了,但愿能给你们踩完这些坑。
这个实际上是个比较蛋疼的点。一开始我发了一段用来检验浏览器兼容性的代码,让合做伙伴(他们负责写原生app嵌入咱们的webview)帮忙先简单地试一下兼容性是否有问题以敲定咱们方案。结果估计是没沟通好,在临近项目上线前,尝试把咱们的页面嵌入时才发现原来丫的不支持这个功能。这算是狠狠踩了坑,后面没办法只能选择临时更换方案,在嵌入IOS的webview使用原生的录音,其余环境逻辑继续走网页录音。
**总结一下,IOS12版本(现阶段最新版本)safari可以支持网页端录音,可是使用wkwebview(原生app嵌入webview)的场景下不支持这个功能。**有看到在github上有人在IOS11时说预估IOS12会支持这个功能。对于咱们而言,这样兼容性比较差的方案确定是绝不留情给它废弃掉。
null is not an object
在safari下咱们针对每一次录音和播放机器人声音的操做都会生成一个audioContext
的实例,在chrome下无论进行多少次操做都没有问题。可是切换到safari后,发现页面最多不能操做5次,只要操做第5次就必然报错。按理说每次的关系应该都是独立的,在确保现象后,找到这篇文章audiocontext Samplerate returning null after being read 8 times。大概意思是,调用失败的缘由是由于audioCtx不能被建立超过6个,不然则会返回null。结合咱们的5次(这个数值可能有必定误差),能够很直观地判断到问题应该就出在这里——咱们的audio示例并无被正常销毁。也就是代码中的audioCtx = null;
并无进入到垃圾回收。一样借助MDN文档,发现这个方法.
AudioContext.close();
关闭一个音频环境, 释听任何正在使用系统资源的音频.
因而过断把audioContext = null
修改为audioContext.close()
完美解决。
在safari下,从远端拉回的音频文件放到audio
标签后,获取总时长显示为Infinity
.可是在chrome下没有这个问题,因而开始定位问题。首先,看这篇文章audio.duration returns Infinity on Safari when mp3 is served from PHP,从文章中的关键信息中提取获得这个问题很大几率是因为请求头设置的问题致使的。因此我尝试把远端的录音文件拉过来放到了egg提供的静态文件目录,经过静态文件的形式进行访问(打算看看请求头应该怎么修改),结果惊喜的发现egg提供的处理静态文件的中间件在safari下能完美运行。这基本就能肯定锅是远端服务没有处理好请求头了。同时看到MDN的文档介绍对dutaion
的介绍.因而能判断到,在chrome下浏览器帮你作了处理(获取到了预设的长度),而safari下须要你本身操做。
A double. If the media data is available but the length is unknown, this value is
NaN
. If the media is streamed and has no predefined length, the value isInf
.
固然看到length
的时候我一度觉得是contentLength
,结果发现最下面的答案中还有一句:
The reason behind why safari returns duration as infinity is quite interesting: It appears that Safari requests the server twice for playing files. First it sends a range request to the server with a range header like this:(bytes:0-1).If the server doesnt’ return the response as a partial content and if it returns the entire stream then the safari browser will not set audio.duration tag and which result in playing the file only once and it can’t be played again.
大概的意思就是在safari下获取音频资源会发送至少两次的请求,第一次请求会形如(bytes: 0-1
),若是服务端没有根据这个请求返回相应的字节内容,那么safari就不会帮你解析下一个请求拿回来的全量音频数据,失去一系列audio标签的功能特性。因而对于请求,咱们能够这么粗糙的解决:
const { ctx } = this;
const file = fs.readFileSync('./record.mp3');
ctx.set('Content-Type', 'audio/mpeg');
if (ctx.headers.range === 'bytes=0-1') {
ctx.set('Content-Range', `bytes 0-1/${file.length}`);
ctx.body = file.slice(0, 1);
} else {
ctx.body = file;
}
复制代码
固然这个处理是很粗糙的处理方式,我反观看了一下koa中间件实现的static-cache
它能在safari下正常运行,可是却没有上面的代码。因此我以为,这上面的代码则是一段偏hack形式的实现。固然如今尚未找到正确的解题思路。
这个问题暂时没有响应的解决方案。只能是把须要修改到子组件的样式提取到不带scope
的style标签上来作到。暂时没有找到比较平滑的兼容方式。
这个实际上是属于原生录音的问题,可是由于一开始觉得是前端的问题因此花了不少时间才把问题定位了出来。记录在这里以防别的小伙伴也踩坑。
在项目中的代码,结束一次的会话会进行各类保存操做和路由跳转操做。可是在接入ios的录音功能后就发现页面的请求虽然是显示已发出,可是后台却迟迟没有收到。——终终于定位到是因为调用了ios的录音中止而致使的这个问题,大概是页面进行一些任务队列相关的操做时就会卡死(若是只是console.log并不会)
这里也稍微贴一下ios的解决方法
// 中止录音队列和移除缓冲区,以及关闭session,这里无需考虑成功与否
AudioQueueStop(_audioQueue, false);
// 移除缓冲区,true表明当即结束录制,false表明将缓冲区处理完再结束
AudioQueueDispose(_audioQueue, false);
复制代码
在嵌入webview后,页面中断的时机,须要将当前正在播放的音频都中断掉。而在ios下执行这个方法会报错(有一些缘由致使须要重复执行)。对于这种报错,选择采用了最简单的try {} catch{}
住,由于在其余状况下都没有,测试了好几种状况应该都没出其余问题
后记,其实吧这段时间还作了不少事情。像什么web-rtc这些,可是一直没时间整理,若是你们有兴趣的话~后面能够整理一下