最近公司安排一个任务,对接IP网络广播,web版的,需要在浏览器直接获取麦克风音频,然后发送给服务端,IP网络广播和服务器是在同一个网段,然后调用第三方进行广播发声。
后端是java,前端是Ant Design Vue,浏览器用的是chrome。
浏览器端是在网上找的采集音频数据的代码,支持16K采样率,16位/样。直播发声肯定要跟服务器及时通信,采用的是websocket,
websocket每次发送数据给服务器有大小限制,具体怎么解除限制我查了不少文档,还是没能解决,暂时就设定150毫秒传输一次,这样可以做到数据不超上限。
Recorder.js是在网上找的,自己改了下数据处理的,这里用的recorder.js。
var time; import axios from '@/utils/request' export default class Recorder { constructor(stream, config) { //兼容 window.URL = window.URL || window.webkitURL; navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; config = config || {}; config.sampleBits = config.sampleBits || 16; //采样数位 8, 16 config.sampleRate = config.sampleRate || 8000; //采样率(1/6 44100) this.context = new (window.webkitAudioContext || window.AudioContext)(); this.audioInput = this.context.createMediaStreamSource(stream); this.createScript = this.context.createScriptProcessor || this.context.createJavaScriptNode; this.recorder = this.createScript.apply(this.context, [4096, 1, 1]); this.audioData = { size: 0, //录音文件长度 buffer: [], //录音缓存 inputSampleRate: this.context.sampleRate, //输入采样率 inputSampleBits: 16, //输入采样数位 8, 16 outputSampleRate: config.sampleRate, //输出采样率 oututSampleBits: config.sampleBits, //输出采样数位 8, 16 input: function (data) { this.buffer.push(new Float32Array(data)); this.size += data.length; }, compress: function () { //合并压缩 //合并 let data = new Float32Array(this.size); let offset = 0; for (let i = 0; i < this.buffer.length; i++) { data.set(this.buffer[i], offset); offset += this.buffer[i].length; } this.buffer = []; this.size = 0; //压缩 let compression = parseInt(this.inputSampleRate / this.outputSampleRate); let length = data.length / compression; let result = new Float32Array(length); let index = 0, j = 0; while (index < length) { result[index] = data[j]; j += compression; index++; } return result; }, encodeWAV: function () { let array = new Array(); let sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate); let sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits); let bytes = this.compress(); let dataLength = bytes.length * (sampleBits / 8); let buffer = new ArrayBuffer(44 + dataLength); let data = new DataView(buffer); let channelCount = 1;//单声道 let offset = 0; let writeString = function (str) { for (let i = 0; i < str.length; i++) { data.setUint8(offset + i, str.charCodeAt(i)); } }; // 资源交换文件标识符 writeString('RIFF'); offset += 4; // 下个地址开始到文件尾总字节数,即文件大小-8 data.setUint32(offset, 36 + dataLength, true); offset += 4; // WAV文件标志 writeString('WAVE'); offset += 4; // 波形格式标志 writeString('fmt '); offset += 4; // 过滤字节,一般为 0x10 = 16 data.setUint32(offset, 16, true); offset += 4; // 格式类别 (PCM形式采样数据) data.setUint16(offset, 1, true); offset += 2; // 通道数 data.setUint16(offset, channelCount, true); offset += 2; // 采样率,每秒样本数,表示每个通道的播放速度 data.setUint32(offset, sampleRate, true); offset += 4; // 波形数据传输率 (每秒平均字节数) 单声道×每秒数据位数×每样本数据位/8 data.setUint32(offset, channelCount * sampleRate * (sampleBits / 8), true); offset += 4; // 快数据调整数 采样一次占用字节数 单声道×每样本的数据位数/8 data.setUint16(offset, channelCount * (sampleBits / 8), true); offset += 2; // 每样本数据位数 data.setUint16(offset, sampleBits, true); offset += 2; // 数据标识符 writeString('data'); offset += 4; // 采样数据总数,即数据总大小-44 data.setUint32(offset, dataLength, true); offset += 4; // 写入采样数据 if (sampleBits === 8) { for (let i = 0; i < bytes.length; i++, offset++) { let s = Math.max(-1, Math.min(1, bytes[i])); let val = s < 0 ? s * 0x8000 : s * 0x7FFF; val = parseInt(255 / (65535 / (val + 32768))); data.setInt8(offset, val, true); } } else { for (let i = 0; i < bytes.length; i++, offset += 2) { let s = Math.max(-1, Math.min(1, bytes[i])); data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true); array.push(parseInt(s < 0 ? s * 0x8000 : s * 0x7FFF)); } } return array; // return new Blob([data], {type: 'audio/wav'}); } }; } //开始录音 start(param) { this.audioInput.connect(this.recorder); this.recorder.connect(this.context.destination); //音频采集 let self = this; this.initWebSocket(); this.sendMsg(self,param); this.recorder.onaudioprocess = function (e) { self.audioData.input(e.inputBuffer.getChannelData(0)); }; }; initWebSocket () { this.websock = new WebSocket('ws://localhost:8083/dispatch/websocket/sendVoice'); this.websock.onopen = this.websocketonopen; this.websock.onerror = this.websocketonerror; this.websock.onmessage = this.websocketonmessage; this.websock.onclose = this.websocketclose; }; websocketonopen () { console.log("WebSocket连接成功"); }; websocketonerror (e) { console.log("WebSocket连接发生错误"); }; websocketsend(data,self){//数据发送 if(self.websock.readyState === self.websock.CLOSED){ // self.restart(self); }else { try { self.websock.send(data); } catch (e) { // self.restart(self); } } }; websocketonmessage (e) { }; websocketclose (e) { try { this.websock.close(); } catch (e1) { console.log(e1); } console.log("connection closed (" + e + ")"); }; sendMsg(self,param){ time = setInterval(function(){ let array = self.audioData.encodeWAV(); self.audioData.buffer = []; self.audioData.size = 0; let json = { id:param.id, pcm:array, }; try { self.websocketsend(JSON.stringify(json), self); } catch (e) { console.log(e); } },150) } restart(self){ clearInterval(time); self.initWebSocket(); self.sendMsg(self); } //停止 stop() { clearInterval(time); this.websock.close(); this.recorder.disconnect(); }; //获取音频文件 getBlob() { this.stop(); // return this.audioData.encodeWAV(); // return this.audioData.compress(); }; //回放 play(audio) { audio.src = window.URL.createObjectURL(this.getBlob()); }; //清理缓存的录音数据 clear() { this.audioData.buffer = []; this.audioData.size = 0; }; static throwError(message) { console.log("Error:" + message); throw new function () { this.toString = function () { return message; } }; }; static canRecording() { return (navigator.getUserMedia != null); } static get(callback, config) { if (callback) { if (Recorder.canRecording()) { navigator.getUserMedia( {audio: true}, //只启用音频 function (stream) { let rec = new Recorder(stream, config); callback(rec); }, function (error) { switch (error.code || error.name) { case 'PERMISSION_DENIED': case 'PermissionDeniedError': Recorder.throwError('用户拒绝提供信息。'); break; case 'NOT_SUPPORTED_ERROR': case 'NotSupportedError': Recorder.throwError('浏览器不支持硬件设备。'); break; case 'MANDATORY_UNSATISFIED_ERROR': case 'MandatoryUnsatisfiedError': Recorder.throwError('无法发现指定的硬件设备。'); break; default: Recorder.throwError('无法打开麦克风。异常信息:' + (error.code || error.name)); break; } }); } else { Recorder.throwError('当前浏览器不支持录音功能。'); return; } } }; }
record-sdk.js
import Recorder from "./Recorder"; export default class Record { startRecord(param) { let self = this; try { Recorder.get(rec => { console.log("init recorder component now."); self.recorder = rec; self.recorder.start(param); console.log("start record now."); param.success("record successfully!"); }); } catch (e) { param.error("record failed!" + e); } } stopRecord() { console.log("stop record now."); let self = this; try { self.recorder.stop(); console.log("stop successfully."); } catch (e) { console.log("stop record failed!" + e); } } play() { console.log("start play record now."); let self = this; try { self.recorder.play(); console.log("play successfully."); } catch (e) { console.log("play record failed!" + e); } } }
页面中引入import Record from "@/api/admin/commons/record-sdk";
export default { data () { return { recorder: new Record(), }methods: { this.recorder.startRecord({ success: res => { this.$message.success("开始喊话"); }, error: res => { console.log("start record failed."); this.$message.error("当前浏览器暂不支持录音"); } }); } }
后端websocket每个150毫秒接收一次pcmdata是16K采样率,16位/样的,由于我用的是第三方的IP网络广播,有提供对接demo,按照需求将pcmdata分割,发送给IP广播,就可以发声了。
新手写博,如果有什么好的建议或者意见可以发消息给我。