1、主題描述
兼容多個(gè)瀏覽器下的前端錄音功能,實(shí)現(xiàn)六大錄音功能:
1、開(kāi)始錄音
2、暫停錄音
3、繼續(xù)錄音
4、結(jié)束錄音
5、播放錄音
6、上傳錄音
2、示例功能
初始狀態(tài):
開(kāi)始錄音:
結(jié)束錄音:
錄音流程 :
示例中的三個(gè)按鈕其實(shí)包含了六個(gè)上述功能,點(diǎn)擊開(kāi)始時(shí)開(kāi)始錄音,可以暫停/結(jié)束錄音,此操作后就可以播放播音/上傳錄音了噢~以下是對(duì)應(yīng)六大錄音功能示例代碼,那大家會(huì)發(fā)現(xiàn)HZRecorder是啥呢? 其實(shí)?HZRecorder 是錄音類,我們調(diào)用的都是該類里面的方法。
那大家肯定好奇,錄音是通過(guò)怎樣一種形式存在呢?其實(shí)用的就是瀏覽器的AudioContext對(duì)象,他旨在創(chuàng)建一個(gè)音頻dom,有輸入和輸出。具體想了解這對(duì)象的,可以去mdn看看
AudioContext
/**
* 錄音前準(zhǔn)備 檢查錄音設(shè)備是否到位
*/
this.readyRecording = async function() {
let recorder // 表示錄音類實(shí)例
// 流模式下ready鉤子 res 為錄音類實(shí)例 或者 false
await HZRecorder.ready().then(res => {
recorder = res
})
return recorder
}
/**
* 開(kāi)始錄音
*/
this.startRecording = function() {
recorder.start();
}
/**
* 結(jié)束錄音
*/
this.stopRecording = function() {
recorder.end();
}
/**
* 播放錄音
*/
this.playRecording = function() {
recorder.play(audio);
}
/**
* 繼續(xù)錄音
*/
this.resumeRecord = function() {
recorder.again();
}
/**
* 暫停錄音
*/
this.pauseRecord = function() {
recorder.stop();
}
/**
* 重新錄音
*/
this.reRecord = function() {
this.startRecording()
}
/**
* 上傳錄音
*/
this.uploadRecord = function() {
// 流模式下上傳
recorder.upload(url, succ, fail)
}
3、流模式下的錄音類
大家看到這標(biāo)題就好奇,啥叫流模式下的錄音類呢?那還有其他模式嗎?的確,我總結(jié)了下,是根據(jù)上傳錄音時(shí)的數(shù)據(jù)來(lái)區(qū)分的~我們常規(guī)情況下,上傳錄音都是流模式,也就是Content-Type為application/octem-stream,源碼如下
/**
* 錄音類(針對(duì)content-type為application/octem-stream 的使用)
* @param {*} stream
* @param {*} config
*/
const HZRecorder = function (stream, config) {
config = config || {};
config.sampleBits = config.sampleBits || 8; //采樣數(shù)位 8, 16
config.sampleRate = config.sampleRate || (44100 / 6); //采樣率(1/6 44100)
//創(chuàng)建一個(gè)音頻環(huán)境對(duì)象
audioContext = window.AudioContext || window.webkitAudioContext;
var context = new audioContext();
//將聲音輸入這個(gè)對(duì)像
var audioInput = context.createMediaStreamSource(stream);
//設(shè)置音量節(jié)點(diǎn)
var volume = context.createGain();
audioInput.connect(volume);
//創(chuàng)建緩存,用來(lái)緩存聲音
var bufferSize = 4096;
// 創(chuàng)建聲音的緩存節(jié)點(diǎn),createScriptProcessor方法的
// 第二個(gè)和第三個(gè)參數(shù)指的是輸入和輸出都是雙聲道。
var recorder = context.createScriptProcessor(bufferSize, 2, 2);
var audioData = {
size: 0 //錄音文件長(zhǎng)度
, buffer: [] //錄音緩存
, inputSampleRate: context.sampleRate //輸入采樣率
, inputSampleBits: 16 //輸入采樣數(shù)位 8, 16
, outputSampleRate: config.sampleRate //輸出采樣率
, oututSampleBits: config.sampleBits //輸出采樣數(shù)位 8, 16
, input: function (data) {
this.buffer.push(new Float32Array(data));
this.size += data.length;
}
, compress: function () { //合并壓縮
//合并
var data = new Float32Array(this.size);
var offset = 0;
for (var i = 0; i < this.buffer.length; i++) {
data.set(this.buffer[i], offset);
offset += this.buffer[i].length;
}
//壓縮
var compression = parseInt(this.inputSampleRate / this.outputSampleRate);
var length = data.length / compression;
var result = new Float32Array(length);
var index = 0, j = 0;
while (index < length) {
result[index] = data[j];
j += compression;
index++;
}
return result;
}
, encodeWAV: function () {
var sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate);
var sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits);
var bytes = this.compress();
var dataLength = bytes.length * (sampleBits / 8);
var buffer = new ArrayBuffer(44 + dataLength);
var data = new DataView(buffer);
var channelCount = 1;//單聲道
var offset = 0;
var writeString = function (str) {
for (var i = 0; i < str.length; i++) {
data.setUint8(offset + i, str.charCodeAt(i));
}
};
// 資源交換文件標(biāo)識(shí)符
writeString('RIFF'); offset += 4;
// 下個(gè)地址開(kāi)始到文件尾總字節(jié)數(shù),即文件大小-8
data.setUint32(offset, 36 + dataLength, true); offset += 4;
// WAV文件標(biāo)志
writeString('WAVE'); offset += 4;
// 波形格式標(biāo)志
writeString('fmt '); offset += 4;
// 過(guò)濾字節(jié),一般為 0x10 = 16
data.setUint32(offset, 16, true); offset += 4;
// 格式類別 (PCM形式采樣數(shù)據(jù))
data.setUint16(offset, 1, true); offset += 2;
// 通道數(shù)
data.setUint16(offset, channelCount, true); offset += 2;
// 采樣率,每秒樣本數(shù),表示每個(gè)通道的播放速度
data.setUint32(offset, sampleRate, true); offset += 4;
// 波形數(shù)據(jù)傳輸率 (每秒平均字節(jié)數(shù)) 單聲道×每秒數(shù)據(jù)位數(shù)×每樣本數(shù)據(jù)位/8
data.setUint32(offset, channelCount * sampleRate * (sampleBits / 8), true); offset += 4;
// 快數(shù)據(jù)調(diào)整數(shù) 采樣一次占用字節(jié)數(shù) 單聲道×每樣本的數(shù)據(jù)位數(shù)/8
data.setUint16(offset, channelCount * (sampleBits / 8), true); offset += 2;
// 每樣本數(shù)據(jù)位數(shù)
data.setUint16(offset, sampleBits, true); offset += 2;
// 數(shù)據(jù)標(biāo)識(shí)符
writeString('data'); offset += 4;
// 采樣數(shù)據(jù)總數(shù),即數(shù)據(jù)總大小-44
data.setUint32(offset, dataLength, true); offset += 4;
// 寫(xiě)入采樣數(shù)據(jù)
if (sampleBits === 8) {
for (var i = 0; i < bytes.length; i++, offset++) {
var s = Math.max(-1, Math.min(1, bytes[i]));
var val = s < 0 ? s * 0x8000 : s * 0x7FFF;
val = parseInt(255 / (65535 / (val + 32768)));
data.setInt8(offset, val, true);
}
} else {
for (var i = 0; i < bytes.length; i++, offset += 2) {
var s = Math.max(-1, Math.min(1, bytes[i]));
data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
}
}
return new Blob([data], { type: 'audio/wav' });
}
};
//開(kāi)始錄音
this.start = function () {
audioInput.connect(recorder);
recorder.connect(context.destination);
};
//停止
this.stop = function () {
recorder.disconnect();
};
// 結(jié)束
this.end = function () {
context.close();
};
// 繼續(xù)
this.again = function () {
recorder.connect(context.destination);
};
//獲取音頻文件
this.getBlob = function () {
this.stop();
return audioData.encodeWAV();
};
//回放
this.play = function (audio) {
audio.src = window.URL.createObjectURL(this.getBlob());
};
//上傳
this.upload = function (url, succ, fail) {
const xhr = new XMLHttpRequest();
xhr.overrideMimeType("application/octet-stream")
// xhr.upload.addEventListener('progress', function (e) {
// }, false);
xhr.addEventListener('load', function (e) {
succ(xhr.response)
}, false);
xhr.addEventListener('error', function (e) {
fail(xhr.response);
}, false);
xhr.addEventListener('abort', function (e) {
fail(xhr.response);
}, false);
xhr.open('POST', url);
if(xhr.sendAsBinary){
xhr.sendAsBinary(this.getBlob());
}else{
xhr.send(this.getBlob());
}
};
//音頻采集
recorder.onaudioprocess = function (e) {
audioData.input(e.inputBuffer.getChannelData(0));
};
}
/**
* 多瀏覽器兼容
* @param {*} videoConfig 參數(shù)配置
* @param {*} succ 成功回調(diào)
* @param {*} fail 失敗回調(diào)
* @returns promise
*/
HZRecorder.compatibleMedia = async function(videoConfig) {
let streamPromise // 視頻promise
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia){
// 最新標(biāo)準(zhǔn)API
streamPromise = await navigator.mediaDevices.getUserMedia(videoConfig)
} else if (navigator.webkitGetUserMedia){
// webkit內(nèi)核瀏覽器
streamPromise = await navigator.webkitGetUserMedia(videoConfig)
} else if (navigator.mozGetUserMedia){
// Firefox瀏覽器
streamPromise = await navagator.mozGetUserMedia(videoConfig)
} else if (navigator.getUserMedia){
// 舊版API
streamPromise = await navigator.getUserMedia(videoConfig)
}
return streamPromise
}
/**
* 是否支持錄音
* @returns 支持直接返回錄音類實(shí)例 : 返回false
*/
HZRecorder.ready = async function() {
let instance // 錄音類實(shí)例(ready ok) | false (ready no)
await HZRecorder.compatibleMedia({ audio: true }).then(stream => {
instance = new HZRecorder(stream);
}).catch(() => {
instance = false
})
return instance
}
4、表單模式下的錄音類
和上述流模式的錄音類有區(qū)別的是,表單模式下適用于上傳錄音時(shí)Content-Type為application/x-www-form-urlencoded噢~
// --------------------------------------------------
/**
* 錄音類(指定content-type為application/x-www-form-urlencoded使用)
* @param {*} stream 流對(duì)象
*/
const HZRecorderForm = function (stream) {
//創(chuàng)建一個(gè)音頻環(huán)境對(duì)象
audioContext = window.AudioContext || window.webkitAudioContext;
var ac = new audioContext();
var chunks = [];
var mediaRecorder
var blobResult
//開(kāi)始錄音
this.start = function () {
if (!mediaRecorder) {
var origin = ac.createMediaStreamSource(stream)
var dest = ac.createMediaStreamDestination();
mediaRecorder = new MediaRecorder(dest.stream);
mediaRecorder.ondataavailable = function(e) {
chunks.push(e.data);
}
mediaRecorder.onstop = function(evt) {
blobResult = new Blob(chunks, { 'type' : 'audio/mpeg' });
};
origin.connect(dest);
}
mediaRecorder.start();
};
// 結(jié)束錄音
this.end = function () {
// 當(dāng)錄音類處于不活躍狀態(tài)時(shí),停止操作
if (mediaRecorder.state === 'inactive') return
mediaRecorder.requestData()
mediaRecorder.stop();
};
// 暫停錄音
this.stop = function() {
// 當(dāng)錄音類處于不活躍狀態(tài)時(shí),停止操作
if (mediaRecorder.state === 'inactive') return
mediaRecorder.pause()
}
// 恢復(fù)錄音
this.again = function() {
// 當(dāng)錄音類處于不活躍狀態(tài)時(shí),停止操作
if (mediaRecorder.state === 'inactive') return
mediaRecorder.resume()
}
//上傳
this.upload = function (url, succ, err) {
setTimeout(() => {
var xhr = new XMLHttpRequest();
xhr.open('POST', url);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
xhr.send(blobResult);
xhr.onload = e => {
// 請(qǐng)求完成 && 外部狀態(tài)碼200 && 內(nèi)部狀態(tài)碼1(這個(gè)內(nèi)部狀態(tài)碼自定義)
if (xhr.readyState === 4 && xhr.status === 200 && JSON.parse(xhr.response).status === 1) {
succ && succ(xhr.response)
} else {
err && err(JSON.parse(xhr.response).message)
}
}
})
};
}
/**
* 多瀏覽器兼容
* @param {*} videoConfig 參數(shù)配置
* @param {*} succ 成功回調(diào)
* @param {*} fail 失敗回調(diào)
* @returns promise
*/
HZRecorderForm.compatibleMedia = async function(videoConfig) {
let streamPromise // 視頻promise
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia){
// 最新標(biāo)準(zhǔn)API
streamPromise = await navigator.mediaDevices.getUserMedia(videoConfig)
} else if (navigator.webkitGetUserMedia){
// webkit內(nèi)核瀏覽器
streamPromise = await navigator.webkitGetUserMedia(videoConfig)
} else if (navigator.mozGetUserMedia){
// Firefox瀏覽器
streamPromise = await navagator.mozGetUserMedia(videoConfig)
} else if (navigator.getUserMedia){
// 舊版API
streamPromise = await navigator.getUserMedia(videoConfig)
}
return streamPromise
}
/**
* 是否支持錄音
* @returns 支持直接返回錄音類實(shí)例 : 返回false
*/
HZRecorderForm.ready = async function() {
let instance // 錄音類實(shí)例(ready ok) | false (ready no)
await HZRecorderForm.compatibleMedia({ audio: true }).then(stream => {
instance = new HZRecorderForm(stream);
}).catch(() => {
instance = false
})
return instance
}
5、疑難解答
1、在錄音開(kāi)始前都必須調(diào)用readyRecording方法嗎?
必須噢,你也可以自己實(shí)現(xiàn)這功能,HZRecorder.ready()方法返回的是promise對(duì)象,其值在當(dāng)前有麥克風(fēng)時(shí)候,返回的是錄音類實(shí)例,你拿到此值就可以調(diào)用錄音類的方法,無(wú)麥克風(fēng)時(shí)候,返回的是false,表示當(dāng)前不具備錄音環(huán)境~
/**
* 錄音前準(zhǔn)備 檢查錄音設(shè)備是否到位
*/
this.readyRecording = async function() {
let recorder // 表示錄音類實(shí)例
// 流模式下ready鉤子 res 為錄音類實(shí)例 或者 false
await HZRecorder.ready().then(res => {
recorder = res
})
// 表單模式下ready鉤子 res 為錄音類實(shí)例 或者 false
await HZRecorderForm.ready().then(res => {
recorder = res
})
return recorder
}
2、火狐瀏覽器提示?navigator.mediaDevices is undefined,找不到?
是的噢,火狐瀏覽器的navigator對(duì)象沒(méi)有mediaDevices這個(gè)屬性,所以這也是我為啥在錄音類里要加入compatibleMedia方法,此方法就是用來(lái)兼容各個(gè)瀏覽器的噢~火狐就是用的navigator.mozGetUserMedia方法
/**
* 多瀏覽器兼容
* @param {*} videoConfig 參數(shù)配置
* @param {*} succ 成功回調(diào)
* @param {*} fail 失敗回調(diào)
* @returns promise
*/
HZRecorder.compatibleMedia = async function(videoConfig) {
let streamPromise // 視頻promise
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia){
// 最新標(biāo)準(zhǔn)API
streamPromise = await navigator.mediaDevices.getUserMedia(videoConfig)
} else if (navigator.webkitGetUserMedia){
// webkit內(nèi)核瀏覽器
streamPromise = await navigator.webkitGetUserMedia(videoConfig)
} else if (navigator.mozGetUserMedia){
// Firefox瀏覽器
streamPromise = await navagator.mozGetUserMedia(videoConfig)
} else if (navigator.getUserMedia){
// 舊版API
streamPromise = await navigator.getUserMedia(videoConfig)
}
return streamPromise
}
3、錄音類的ready方法,為啥要使用async/await呢?
這個(gè)就有點(diǎn)涉及異步的知識(shí)了,在一個(gè)異步函數(shù)里,return是屬于同步邏輯噢,promise.then屬于異步,所以 return 會(huì)先于 .then執(zhí)行的噢,這就和我們的想法不一致了,所以要await 阻塞代碼,拿到instance值了,再返回
/**
* 是否支持錄音
* @returns 支持直接返回錄音類實(shí)例 : 返回false
*/
HZRecorder.ready = async function() {
let instance // 錄音類實(shí)例(ready ok) | false (ready no)
await HZRecorder.compatibleMedia({ audio: true }).then(stream => {
instance = new HZRecorder(stream);
}).catch(() => {
instance = false
})
return instance
}
4、錄音類為啥要使用?XMLHttpRequest 來(lái)觸發(fā)接口呢?
不一定要使用原生xhr噢,你也可以根據(jù)你需求來(lái)修改成axios/fetch/ajax等~這個(gè)不影響整體代碼的使用
5、流模式和表單模式的錄音類本質(zhì)上有啥區(qū)別?
其實(shí)在外層是上傳接口的請(qǐng)求頭區(qū)別,但在實(shí)際上,只是由于流模式下的寫(xiě)法,無(wú)法將音頻轉(zhuǎn)成mp3格式(默認(rèn)為wav格式),當(dāng)然網(wǎng)上也有小伙伴認(rèn)為引入lame庫(kù)來(lái)實(shí)現(xiàn)wav轉(zhuǎn)換mp3的操作,當(dāng)然可以啦~這不影響,只是對(duì)我來(lái)說(shuō),我是能不引入第三方庫(kù)就不引入。
而表單模式實(shí)際上用的瀏覽器支持的另一個(gè)接口?MediaRecorder
而MediaRecorder是專門(mén)來(lái)做錄制的,他想轉(zhuǎn)換格式的話,就簡(jiǎn)單的多,在錄制完觸發(fā)onstop時(shí),將可以將二進(jìn)制數(shù)據(jù)轉(zhuǎn)換成任意想要的格式,audio/mpeg就是mp3的格式~
mediaRecorder.ondataavailable = function(e) {
chunks.push(e.data);
}
mediaRecorder.onstop = function(evt) {
blobResult = new Blob(chunks, { 'type' : 'audio/mpeg' });
};
6、表單模式下的錄音類為啥要判斷inactive狀態(tài)呢?
因?yàn)?
MediaRecorder?接口 有三個(gè)狀態(tài)?inactive,?recording,?paused
這三個(gè)狀態(tài)分別是設(shè)備閑置,設(shè)備使用,設(shè)備暫停,有點(diǎn)類似于window的未響應(yīng),當(dāng)我們想要操作麥克風(fēng)時(shí),此時(shí)麥克風(fēng)inactive了,那就無(wú)法響應(yīng)我們的請(qǐng)求,所以當(dāng)狀態(tài)為inactive時(shí),我們都return掉,使他不執(zhí)行我們的方法。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-764582.html
// 結(jié)束錄音
this.end = function () {
// 當(dāng)錄音類處于不活躍狀態(tài)時(shí),停止操作
if (mediaRecorder.state === 'inactive') return
mediaRecorder.requestData()
mediaRecorder.stop();
};
// 暫停錄音
this.stop = function() {
// 當(dāng)錄音類處于不活躍狀態(tài)時(shí),停止操作
if (mediaRecorder.state === 'inactive') return
mediaRecorder.pause()
}
// 恢復(fù)錄音
this.again = function() {
// 當(dāng)錄音類處于不活躍狀態(tài)時(shí),停止操作
if (mediaRecorder.state === 'inactive') return
mediaRecorder.resume()
}
------有不懂的可以評(píng)論區(qū)聊聊噢~------文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-764582.html
到了這里,關(guān)于有趣且重要的JS知識(shí)合集(18)瀏覽器實(shí)現(xiàn)前端錄音功能的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!