日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

基于H5的实时语音聊天

發(fā)布時(shí)間:2023/12/14 编程问答 44 豆豆
生活随笔 收集整理的這篇文章主要介紹了 基于H5的实时语音聊天 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

#基于H5的實(shí)時(shí)語(yǔ)音聊天

業(yè)務(wù)需求:網(wǎng)頁(yè)和移動(dòng)端的通訊,移動(dòng)端播放g711alaw,難點(diǎn)如下:

  • 網(wǎng)頁(yè)如何調(diào)用系統(tǒng)api錄音
  • 錄音后的數(shù)據(jù)是什么格式?如何轉(zhuǎn)碼?
  • 如何實(shí)時(shí)通訊

<input type="text" id="a"/> <button id="b">buttonB</button> <button id="c">停止</button>******************************************var a = document.getElementById('a'); var b = document.getElementById('b'); var c = document.getElementById('c');navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia;var gRecorder = null; var audio = document.querySelector('audio'); var door = false; var ws = null;b.onclick = function() {if(a.value === '') {alert('請(qǐng)輸入用戶名');return false;}if(!navigator.getUserMedia) {alert('抱歉您的設(shè)備無(wú)法語(yǔ)音聊天');return false;}SRecorder.get(function (rec) {gRecorder = rec;});ws = new WebSocket("wss://x.x.x.x:8888");ws.onopen = function() {console.log('握手成功');ws.send('user:' + a.value);};ws.onmessage = function(e) {receive(e.data);};document.onkeydown = function(e) {if(e.keyCode === 65) {if(!door) {gRecorder.start();door = true;}}};document.onkeyup = function(e) {if(e.keyCode === 65) {if(door) {ws.send(gRecorder.getBlob());gRecorder.clear();gRecorder.stop();door = false;}}} }c.onclick = function() {if(ws) {ws.close();} }var SRecorder = function(stream) {config = {};config.sampleBits = config.smapleBits || 8; //輸出采樣位數(shù)config.sampleRate = config.sampleRate || (44100 / 6); //輸出采樣頻率var context = new AudioContext();var audioInput = context.createMediaStreamSource(stream);var recorder = context.createScriptProcessor(4096, 1, 1); //錄音緩沖區(qū)大小,輸入通道數(shù),輸出通道數(shù)var audioData = {size: 0 //錄音文件長(zhǎng)度, buffer: [] //錄音緩存, inputSampleRate: context.sampleRate //輸入采樣率, inputSampleBits: 16 //輸入采樣數(shù)位 8, 16, outputSampleRate: config.sampleRate //輸出采樣率, oututSampleBits: config.sampleBits //輸出采樣數(shù)位 8, 16, clear: function() {this.buffer = [];this.size = 0;}, 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è)地址開始到文件尾總字節(jié)數(shù),即文件大小-8data.setUint32(offset, 36 + dataLength, true); offset += 4;// WAV文件標(biāo)志writeString('WAVE'); offset += 4;// 波形格式標(biāo)志writeString('fmt '); offset += 4;// 過(guò)濾字節(jié),一般為 0x10 = 16data.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ù)位/8data.setUint32(offset, channelCount * sampleRate * (sampleBits / 8), true); offset += 4;// 快數(shù)據(jù)調(diào)整數(shù) 采樣一次占用字節(jié)數(shù) 單聲道×每樣本的數(shù)據(jù)位數(shù)/8data.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ù)總大小-44data.setUint32(offset, dataLength, true); offset += 4;// 寫入采樣數(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' });}};this.start = function () {audioInput.connect(recorder);recorder.connect(context.destination);}this.stop = function () {recorder.disconnect();}this.getBlob = function () {return audioData.encodeWAV();}this.clear = function() {audioData.clear();}recorder.onaudioprocess = function (e) {audioData.input(e.inputBuffer.getChannelData(0));} };SRecorder.get = function (callback) {if (callback) {if (navigator.getUserMedia) {navigator.getUserMedia({ audio: true },function (stream) {var rec = new SRecorder(stream);callback(rec);})}} }function receive(e) {audio.src = window.URL.createObjectURL(e); }

上面是一段調(diào)用H5 Api發(fā)送語(yǔ)音的代碼片段,按住A鍵錄音,松開A鍵發(fā)送,實(shí)現(xiàn)功能類似于微信,下面先對(duì)上面代碼進(jìn)行分析和理解,便于實(shí)現(xiàn)我們的實(shí)時(shí)語(yǔ)音需求。

首先創(chuàng)建AudioContext對(duì)象,有這個(gè)對(duì)象才有后面的H5調(diào)用底層硬件功能,
var recorder = context.createScriptProcessor(4096, 1, 1);
這句話創(chuàng)建的4096是錄音一次錄多少個(gè)字節(jié),錄到4096字節(jié)后會(huì)走后面的回調(diào):

recorder.onaudioprocess = function (e) {audioData.input(e.inputBuffer.getChannelData(0));}

按住A鍵后調(diào)用gRecorder.start();

this.start = function () {audioInput.connect(recorder);recorder.connect(context.destination); //把麥克風(fēng)的輸入和音頻采集相連起來(lái) context.destination返回代表在環(huán)境中的音頻的最終目的地。}

然后走上面的回調(diào)不停的采集麥克風(fēng)的音頻數(shù)據(jù)input進(jìn)去,input在這里:

input: function (data) {this.buffer.push(new Float32Array(data)); //buffer存儲(chǔ)float32,size為字節(jié)大小,buffer大小為float大小this.size += data.length;}

這里將字節(jié)流數(shù)據(jù),存成Float32Array存在buffer中,也就是說(shuō)成員變量里面的buffer實(shí)際上是多個(gè)Float32Array組成的

合并和壓縮

因?yàn)椴煌臉I(yè)務(wù)可能要將錄制的PCM轉(zhuǎn)成需要的音頻格式,所以了解錄制的音頻是什么格式很有必要

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); //把buffer中的各個(gè)Float32數(shù)組合并成一個(gè)offset += this.buffer[i].length;}//壓縮var compression = parseInt(this.inputSampleRate / this.outputSampleRate); //6var length = data.length / compression; //600/6 = 10var result = new Float32Array(length); // 32 位浮點(diǎn)值的類型化數(shù)組var index = 0, j = 0;while (index < length) {result[index] = data[j];j += compression; //j+=6index++;}return result;}

合并,不多說(shuō),就是將buffer中的各個(gè)Float32Array合并成一個(gè),壓縮,根據(jù)輸入采樣率和輸出采樣率計(jì)算出一個(gè)比例,像我的項(xiàng)目中輸入采樣頻率為48000 我計(jì)劃生成的音頻采樣頻率為8000 那這里的比值為6,計(jì)算出比值后,每隔這個(gè)比值6采樣一次,平均取點(diǎn),(經(jīng)測(cè)試這樣的簡(jiǎn)單取點(diǎn)不會(huì)有雜音)。
這里再啰嗦下音頻的基本知識(shí):
采樣頻率:一秒鐘采樣多少次
采樣位數(shù):一次采樣多大 如果16bit那就是一次采樣2byte
聲道數(shù)(通道數(shù)):幾個(gè)通道采樣
這樣計(jì)算下來(lái)你一秒鐘采樣的大小為:聲道數(shù) * 采樣頻率 * 采樣位數(shù)/8 (單位:byte)
所以上面的4096字節(jié)收集一次數(shù)據(jù)大概用了多少秒就可以計(jì)算出來(lái)了 :4096/一秒鐘采樣大小 (單位:s)

編碼

前言:
PCM:電腦錄制出的原聲,未經(jīng)壓縮,聲音還原度高,文件較大
wav:符合RIFF標(biāo)準(zhǔn)的音頻文件,不具體只某一種音頻編碼算法,如wav可以有PCM編碼的wav,g711編碼的wav…等等,只要他符合RIFF標(biāo)準(zhǔn)
wav頭:那么如何叫符合RIFF呢?自己百度。。。wav文件有44個(gè)字節(jié)的頭,頭里面告訴你音頻是什么格式的,音頻數(shù)據(jù)有多大。。。其實(shí)就是一對(duì)符合RIFF的結(jié)構(gòu)體
這里面編碼成wav,為什么要編碼成wav呢,因?yàn)樗蟁IFF標(biāo)準(zhǔn),在H5的頁(yè)面能播放,轉(zhuǎn)成其他音頻也方便。因?yàn)槲覀冧浿频穆曇粑募荘CM格式的
我們錄制的PCM數(shù)據(jù)太大了,也不方便傳輸,那么就根據(jù)需要壓縮了一下,再編碼成wav,根據(jù)自己需要,如果不需要轉(zhuǎn)成wav,那么直接將PCM發(fā)送出去就好,后臺(tái)再對(duì)數(shù)據(jù)進(jìn)行處理。

###發(fā)送方式與后臺(tái)接收

發(fā)送方式基于websocket,我這里后臺(tái)接收用的java,需要注意兩點(diǎn):1.注意接收緩沖區(qū)大小設(shè)置足夠2.注意捕獲onclose里面的reson @OnMessage(maxMessageSize=160000) //最大160000字節(jié)public void OnMessage2(byte[] message, Session session) {logger.info("byte:" + message.length);System.out.println("轉(zhuǎn)化為16進(jìn)制:"+byteArrayToHexStr(message));} @OnClosepublic void onClose(Session session, CloseReason reason) {connList.remove(this);logger.error("onclose被調(diào)用"+reason.toString());}

如何實(shí)時(shí)播放語(yǔ)音?

有兩種播放聲音的方法:

1. audio.src = window.URL.createObjectURL(e); 2.audioContext.decodeAudioData(play_queue[index_play], function(buffer) {//解碼成pcm流var audioBufferSouceNode = audioContext.createBufferSource();audioBufferSouceNode.buffer = buffer;audioBufferSouceNode.connect(audioContext.destination);audioBufferSouceNode.start(0);}, function(e) {console.log("failed to decode the file");});

這里很明顯第一種對(duì)于實(shí)時(shí)播放并不管用,因?yàn)檎Z(yǔ)音包來(lái)的太頻繁了,不停得變換src是有聲音卡頓的,第二種親測(cè),有效
這里注意啦:decodeAudioData這個(gè)Api很強(qiáng)大,從后臺(tái)回傳來(lái)的數(shù)據(jù),用前臺(tái)那個(gè)encodeWAV編碼成wav后,傳入decodeAudioData,是可以轉(zhuǎn)換成聲卡直接播放的數(shù)據(jù)的,也就是說(shuō)大家不用絞盡腦汁思考如何把后臺(tái)傳來(lái)的數(shù)據(jù)變成Float32Array了,沒(méi)那個(gè)必要。
再一個(gè)這里用了個(gè)循環(huán)緩沖隊(duì)列,將回傳來(lái)編碼后的音頻存在循環(huán)緩沖隊(duì)列中,而播放線程叢這個(gè)隊(duì)列里取數(shù)據(jù):play_queue[index_play],給大家提供個(gè)思路,有人有需要再貼代碼吧。。

老東家代碼不能上傳,只有測(cè)試demo
前后端demo: https://download.csdn.net/download/qq422243639/10734859

如不清楚轉(zhuǎn)碼是否正確,在使用轉(zhuǎn)碼方法轉(zhuǎn)碼后,寫入文件,用coolpro2 這個(gè)工具打開,選擇碼率,比特率等,然后打開,聽聽語(yǔ)音是否清晰,工具下載地址:
https://download.csdn.net/download/qq422243639/10734845

附贈(zèng)IE基于ActiveX插件的實(shí)時(shí)語(yǔ)音解決方式:
https://download.csdn.net/download/qq422243639/10734877

總結(jié)

以上是生活随笔為你收集整理的基于H5的实时语音聊天的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。