司机需要通过车载终端设备与WEB平台进行语音对讲,目前已实现WebSocket服务端,需要实现客户端
说到websocket想比大家不会陌生,如果陌生的话也没关系,一句话概括
WebSocket protocol 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信
我们需要在web端实现双向对讲,首先创建html文件,这个只是用来做个demo,比较简单:
test
第一个是实时输出音频数据,第二是实时解析输入的音频流并进行播放
这里要介绍一下Navigator.getUserMedia()方法,点我查看API文档
该方法提醒用户需要使用的设备类型,音频(true或者false)和视频(true或者false),比如麦克风。
如下例,我们使用音频输入,获取mediaStream,构建相关业务对象:
function init(rec){
record = red;
}
if (!navigator.getUserMedia) {
alert('浏览器不支持音频输入');
}else{
navigator.getUserMedia(
{ audio: true },
function (mediaStream) {
init(new Recorder(mediaStream));
},function(error){
console.log(error)
}
)
}
//录音对象
var Recorder = function(stream) {
var sampleBits = 16;//输出采样数位 8, 16
var sampleRate = 8000;//输出采样率
var context = new AudioContext();
var audioInput = context.createMediaStreamSource(stream);
var recorder = context.createScriptProcessor(4096, 1, 1);
var audioData = {
size: 0 //录音文件长度
, buffer: [] //录音缓存
, inputSampleRate: sampleRate //输入采样率
, inputSampleBits: 16 //输入采样数位 8, 16
, outputSampleRate: sampleRate
, oututSampleBits: sampleBits
, 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;
}, encodePCM: 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(dataLength);
var data = new DataView(buffer);
var offset = 0;
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]);
}
};
this.start = function () {
audioInput.connect(recorder);
recorder.connect(context.destination);
}
this.stop = function () {
recorder.disconnect();
}
this.getBlob = function () {
return audioData.encodePCM();
}
this.clear = function() {
audioData.clear();
}
recorder.onaudioprocess = function (e) {
audioData.input(e.inputBuffer.getChannelData(0));
}
};
至此我们可以拿到麦克风输入的音频数据,但是要实现语音对讲,还需要进行一些处理,将数据实时输出到服务器,我这边使用了定时器,每隔500毫秒发送一次语音数据给服务器,代码如下:
begin.onclick = function() {
var ws = new WebSocket("ws://192.168.168.4:6604");
ws.onopen = function() {
console.log('握手成功');
//业务命令构建
var data = {
"cmd": "jtv",//发送命令
"id": "018665897939",//发送设备id
"type": 1,//对讲类型
"channel": 0//语音通道
}
ws.send(JSON.stringify(data));
};
timeInte=setInterval(function(){
if(gRecorder&&ws.readyState==1){//ws进入连接状态,则每隔500毫秒发送一包数据
record.start();
ws.send(record.getBlob());
record.clear(); //每次发送完成则清理掉旧数据
}
},500);
}
注意:该功能前置条件是已经实现WebSocket服务器端。
接上以上示例,我们已经实现了WebSocket,服务器端会实时返回二进制数据到前端,我们需要对数据进行解析和处理,转码成WAV格式进行播放,废话不多说,先上代码:
ws.onmessage = function(e) {
receive(e.data);
};
由于websocket属于长连接,所以我们这边再也不需要进行传统的轮询等复杂操作,只需要实现onmessage即可实时获取服务器发送过来的数据,服务器首先会返回一串字符串,对我们发送的命令进行回馈,OK之后才会开始发送音频数据给我们,ws支持字符串和二进制数据发送,音频数据自然属于后者,所以我们需要对数据进行解析,这里我们用到了window.AudioContext ,介绍一下它吧,AudioContext接口是一个音频上下文对象,表示由音频模块连接而成的音频处理对象,AudioContext可以控制它所包含的节点的创建,以及音频处理、解码操作的执行。
接下来我们进行二进制数据的解析和播放:
function receive(data) {
if( typeof e == 'string' && JSON.parse(e).message=='OK'){
console.log('OK');
}else{
var buffer = (new Response(data)).arrayBuffer();
buffer.then(function(buf){
var audioContext = new ( window.AudioContext || window.webkitAudioContext )();
var fileResult =addWavHeader(buf, '8000', '16', '1');//解析数据转码wav
audioContext.decodeAudioData(fileResult, function(buffer) {
_visualize(audioContext,buffer);//播放
});
});
}
}
DataView 视图是一个可以从 ArrayBuffer 对象中读写多种数值类型的底层接口,使用它时,不用考虑不同平台的字节序问题。
详细介绍点我
//处理音频流,转码wav
var addWavHeader = function(samples,sampleRateTmp,sampleBits,channelCount){
var dataLength = samples.byteLength;
var buffer = new ArrayBuffer(44 + dataLength);
var view = new DataView(buffer);
function writeString(view, offset, string){
for (var i = 0; i < string.length; i++){
view.setUint8(offset + i, string.charCodeAt(i));
}
}
var offset = 0;
/* 资源交换文件标识符 */
writeString(view, offset, 'RIFF'); offset += 4;
/* 下个地址开始到文件尾总字节数,即文件大小-8 */
view.setUint32(offset, /*32*/ 36 + dataLength, true); offset += 4;
/* WAV文件标志 */
writeString(view, offset, 'WAVE'); offset += 4;
/* 波形格式标志 */
writeString(view, offset, 'fmt '); offset += 4;
/* 过滤字节,一般为 0x10 = 16 */
view.setUint32(offset, 16, true); offset += 4;
/* 格式类别 (PCM形式采样数据) */
view.setUint16(offset, 1, true); offset += 2;
/* 通道数 */
view.setUint16(offset, channelCount, true); offset += 2;
/* 采样率,每秒样本数,表示每个通道的播放速度 */
view.setUint32(offset, sampleRateTmp, true); offset += 4;
/* 波形数据传输率 (每秒平均字节数) 通道数×每秒数据位数×每样本数据位/8 */
view.setUint32(offset, sampleRateTmp * channelCount * (sampleBits / 8), true); offset +=4;
/* 快数据调整数 采样一次占用字节数 通道数×每样本的数据位数/8 */
view.setUint16(offset, channelCount * (sampleBits / 8), true); offset += 2;
/* 每样本数据位数 */
view.setUint16(offset, sampleBits, true); offset += 2;
/* 数据标识符 */
writeString(view, offset, 'data'); offset += 4;
/* 采样数据总数,即数据总大小-44 */
view.setUint32(offset, dataLength, true); offset += 4;
function floatTo32BitPCM(output, offset, input){
input = new Int32Array(input);
for (var i = 0; i < input.length; i++, offset+=4){
output.setInt32(offset,input[i],true);
}
}
function floatTo16BitPCM(output, offset, input){
input = new Int16Array(input);
for (var i = 0; i < input.length; i++, offset+=2){
output.setInt16(offset,input[i],true);
}
}
function floatTo8BitPCM(output, offset, input){
input = new Int8Array(input);
for (var i = 0; i < input.length; i++, offset++){
output.setInt8(offset,input[i],true);
}
}
if(sampleBits == 16){
floatTo16BitPCM(view, 44, samples);
}else if(sampleBits == 8){
floatTo8BitPCM(view, 44, samples);
}else{
floatTo32BitPCM(view, 44, samples);
}
return view.buffer;
}
//播放音频
var _visualize = function(audioContext, buffer) {
var audioBufferSouceNode = audioContext.createBufferSource(),
analyser = audioContext.createAnalyser(),
that = this;
//将信号源连接到分析仪
audioBufferSouceNode.connect(analyser);
//将分析仪连接到目的地(扬声器),否则我们将听不到声音
analyser.connect(audioContext.destination);
//然后将缓冲区分配给缓冲区源节点
audioBufferSouceNode.buffer = buffer;
//发挥作用
if (!audioBufferSouceNode.start) {
audioBufferSouceNode.start = audioBufferSouceNode.noteOn //在旧浏览器中使用noteOn方法
audioBufferSouceNode.stop = audioBufferSouceNode.noteOff //在旧浏览器中使用noteOff方法
};
//如果有的话,停止前一个声音
if (this.animationId !== null) {
cancelAnimationFrame(this.animationId);
}
audioBufferSouceNode.start(0);
audo.source = audioBufferSouceNode;
audo.audioContext = audioContext;
}
到此,我们就实现了双向对讲全部功能。
本篇文章也参考以下资料:
https://blog.csdn.net/tiantangyouzui/article/details/71083330