隨着公司產品的業務擴展,今年算是和瀏覽器的錄音功能硬磕上了。遇到了很多奇葩的問題以及一些更多的擴展吧~這裏記錄一下分享給一樣遇到問題後腦袋疼的各位。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這些,可是一直沒時間整理,若是你們有興趣的話~後面能夠整理一下