菜单

websocket搜求其与语音、图片的技艺

2019年9月22日 - Bootstrap

websocket探求其与语音、图片的力量

2015/12/26 · JavaScript
· 3 评论 ·
websocket

原稿出处:
AlloyTeam   

聊到websocket想比大家不会面生,假使素不相识的话也没提到,一句话回顾

“WebSocket protocol
是HTML5一种新的合计。它达成了浏览器与服务器全双工通讯”

WebSocket相比较古板那个服务器推技能几乎好了太多,大家得以挥手向comet和长轮询那些技巧说拜拜啦,庆幸大家生活在富有HTML5的一世~

那篇小说大家将分三部分查究websocket

先是是websocket的科学普及使用,其次是一心本人成立服务器端websocket,最后是入眼介绍利用websocket制作的多个demo,传输图片和在线语音聊天室,let’s
go

一、websocket常见用法

那边介绍三种自个儿认为大面积的websocket实现……(潜心:本文建设构造在node上下文情状

1、socket.io

先给demo

JavaScript

var http = require(‘http’); var io = require(‘socket.io’); var server =
http.createServer(function(req, res) { res.writeHeader(200,
{‘content-type’: ‘text/html;charset=”utf-8″‘}); res.end();
}).listen(8888); var socket =.io.listen(server);
socket.sockets.on(‘connection’, function(socket) { socket.emit(‘xxx’,
{options}); socket.on(‘xxx’, function(data) { // do someting }); });

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var http = require(‘http’);
var io = require(‘socket.io’);
 
var server = http.createServer(function(req, res) {
    res.writeHeader(200, {‘content-type’: ‘text/html;charset="utf-8"’});
    res.end();
}).listen(8888);
 
var socket =.io.listen(server);
 
socket.sockets.on(‘connection’, function(socket) {
    socket.emit(‘xxx’, {options});
 
    socket.on(‘xxx’, function(data) {
        // do someting
    });
});

信任领悟websocket的同桌不大概不知晓socket.io,因为socket.io太盛名了,也很棒,它自个儿对过期、握手等都做了管理。小编推断那也是促成websocket使用最多的措施。socket.io最最最精美的一点正是优雅降级,当浏览器不帮衬websocket时,它会在其间优雅降级为长轮询等,客商和开采者是没有必要关注具体达成的,很有利。

不过工作是有两面性的,socket.io因为它的通盘也推动了坑的地点,最注重的正是臃肿,它的卷入也给多少拉动了很多的通信冗余,而且优雅降级这一独到之处,也随同浏览器标准化的展开逐级失去了惊天动地

Chrome Supported in version 4+
Firefox Supported in version 4+
Internet Explorer Supported in version 10+
Opera Supported in version 10+
Safari Supported in version 5+

在此间不是责问说socket.io不佳,已经被淘汰了,而是不时候我们也得以设想部分另外的兑现~

 

2、http模块

正要说了socket.io臃肿,那现在就来讲说便捷的,首先demo

JavaScript

var http = require(‘http’); var server = http.createServer();
server.on(‘upgrade’, function(req) { console.log(req.headers); });
server.listen(8888);

1
2
3
4
5
6
var http = require(‘http’);
var server = http.createServer();
server.on(‘upgrade’, function(req) {
console.log(req.headers);
});
server.listen(8888);

很简短的兑现,其实socket.io内部对websocket也是如此落成的,不过后边帮大家封装了有个别handle管理,这里我们也足以友善去丰裕,给出两张socket.io中的源码图

图片 1

图片 2

 

3、ws模块

背后有个例证会用到,这里就提一下,前面具体看~

 

二、本人完成一套server端websocket

恰好说了二种常见的websocket完毕格局,今后我们寻思,对于开垦者来说

websocket相对于古板http数据交互方式以来,扩充了服务器推送的风波,客商端接收到事件再张开对应管理,开荒起来分裂实际不是太大呀

那是因为那一个模块已经帮大家将数量帧分析此间的坑都填好了,第3局地我们将尝试本身营造一套简便的服务器端websocket模块

谢谢次碳酸钴的研究帮忙,本身在这里那有的只是简单说下,假设对此有意思味好奇的请百度【web技术研讨所】

投机完结服务器端websocket主要有两点,二个是利用net模块接受数据流,还会有多个是对待官方的帧结构图分析数据,完结这两局地就曾经造成了方方面面包车型大巴平底专门的学问

第一给贰个顾客端发送websocket握手报文的抓包内容

客户端代码很粗大略

JavaScript

ws = new WebSocket(“ws://127.0.0.1:8888”);

1
ws = new WebSocket("ws://127.0.0.1:8888");

图片 3

劳务器端要针对性那么些key验证,正是讲key加上贰个一定的字符串后做三次sha1运算,将其结果转变为base64送回去

JavaScript

var crypto = require(‘crypto’); var WS =
‘258EAFA5-E914-47DA-95CA-C5AB0DC85B11’;
require(‘net’).createServer(function(o) { var key;
o.on(‘data’,function(e) { if(!key) { // 获取发送过来的KEY key =
e.toString().match(/Sec-WebSocket-Key: (.+)/)[1]; //
连接上WS那么些字符串,并做叁回sha1运算,最终转变来Base64 key =
crypto.createHash(‘sha1’).update(key+WS).digest(‘base64’); //
输出再次回到给顾客端的数量,那些字段都以必得的 o.write(‘HTTP/1.1 101
Switching Protocols\r\n’); o.write(‘Upgrade: websocket\r\n’);
o.write(‘Connection: Upgrade\r\n’); // 这几个字段带上服务器管理后的KEY
o.write(‘Sec-WebSocket-Accept: ‘+key+’\r\n’); //
输出空行,使HTTP头停止 o.write(‘\r\n’); } }); }).listen(8888);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var crypto = require(‘crypto’);
var WS = ‘258EAFA5-E914-47DA-95CA-C5AB0DC85B11’;
 
require(‘net’).createServer(function(o) {
var key;
o.on(‘data’,function(e) {
if(!key) {
// 获取发送过来的KEY
key = e.toString().match(/Sec-WebSocket-Key: (.+)/)[1];
// 连接上WS这个字符串,并做一次sha1运算,最后转换成Base64
key = crypto.createHash(‘sha1’).update(key+WS).digest(‘base64’);
// 输出返回给客户端的数据,这些字段都是必须的
o.write(‘HTTP/1.1 101 Switching Protocols\r\n’);
o.write(‘Upgrade: websocket\r\n’);
o.write(‘Connection: Upgrade\r\n’);
// 这个字段带上服务器处理后的KEY
o.write(‘Sec-WebSocket-Accept: ‘+key+’\r\n’);
// 输出空行,使HTTP头结束
o.write(‘\r\n’);
}
});
}).listen(8888);

如此那般握手部分就已经到位了,后边正是多少帧解析与调换的活了

先看下官方提供的帧结构暗示图

图片 4

简单来说介绍下

FIN为是或不是终止的标示

ENVISIONSV为留下空间,0

opcode标记数据类型,是还是不是分片,是不是二进制深入分析,心跳包等等

付给一张opcode对应图

图片 5

MASK是不是利用掩码

Payload len和前边extend payload length表示数据长度,那么些是最麻烦的

PayloadLen唯有7位,换来无符号整型的话独有0到127的取值,这么小的数值当然不可能描述相当的大的数码,因而显明当数码长度小于或等于125时候它才作为数据长度的叙说,要是那一个值为126,则时候背后的多少个字节来存款和储蓄数据长度,假使为127则用前面多少个字节来积攒数据长度

Masking-key掩码

下边贴出深入分析数据帧的代码

JavaScript

function decodeDataFrame(e) { var i = 0, j,s, frame = { FIN: e[i]
>> 7, Opcode: e[i++] & 15, Mask: e[i] >> 7,
PayloadLength: e[i++] & 0x7F }; if(frame.PayloadLength === 126) {
frame.PayloadLength = (e[i++] << 8) + e[i++]; }
if(frame.PayloadLength === 127) { i += 4; frame.PayloadLength =
(e[i++] << 24) + (e[i++] << 16) + (e[i++] << 8)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function decodeDataFrame(e) {
var i = 0,
j,s,
frame = {
FIN: e[i] >> 7,
Opcode: e[i++] & 15,
Mask: e[i] >> 7,
PayloadLength: e[i++] & 0x7F
};
 
if(frame.PayloadLength === 126) {
frame.PayloadLength = (e[i++] << 8) + e[i++];
}
 
if(frame.PayloadLength === 127) {
i += 4;
frame.PayloadLength = (e[i++] << 24) + (e[i++] << 16) + (e[i++] << 8) + e[i++];
}
 
if(frame.Mask) {
frame.MaskingKey = [e[i++], e[i++], e[i++], e[i++]];
 
for(j = 0, s = []; j < frame.PayloadLength; j++) {
s.push(e[i+j] ^ frame.MaskingKey[j%4]);
}
} else {
s = e.slice(i, i+frame.PayloadLength);
}
 
s = new Buffer(s);
 
if(frame.Opcode === 1) {
s = s.toString();
}
 
frame.PayloadData = s;
return frame;
}

下一场是转换数据帧的

JavaScript

function encodeDataFrame(e) { var s = [], o = new
Buffer(e.PayloadData), l = o.length; s.push((e.FIN << 7) +
e.Opcode); if(l < 126) { s.push(l); } else if(l < 0x10000) {
s.push(126, (l&0xFF00) >> 8, l&0xFF); } else { s.push(127, 0, 0,
0, 0, (l&0xFF000000) >> 24, (l&0xFF0000) >> 16, (l&0xFF00)
>> 8, l&0xFF); } return Buffer.concat([new Buffer(s), o]); }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function encodeDataFrame(e) {
var s = [],
o = new Buffer(e.PayloadData),
l = o.length;
 
s.push((e.FIN << 7) + e.Opcode);
 
if(l < 126) {
s.push(l);
} else if(l < 0x10000) {
s.push(126, (l&0xFF00) >> 8, l&0xFF);
} else {
s.push(127, 0, 0, 0, 0, (l&0xFF000000) >> 24, (l&0xFF0000) >> 16, (l&0xFF00) >> 8, l&0xFF);
}
 
return Buffer.concat([new Buffer(s), o]);
}

都以遵照帧结构暗指图上的去管理,在此间不细讲,文章首要在下局地,假设对那块感兴趣的话能够移动web本事研商所~

 

三、websocket传输图片和websocket语音聊天室

正片环节到了,那篇小说最根本的大概显得一下websocket的一对应用情况

1、传输图片

咱俩先思虑传输图片的步骤是哪些,首先服务器收到到顾客端诉求,然后读取图片文件,将二进制数据转发给客户端,客商端如何管理?当然是行使File里德r对象了

先给客商端代码

JavaScript

var ws = new WebSocket(“ws://xxx.xxx.xxx.xxx:8888”); ws.onopen =
function(){ console.log(“握手成功”); }; ws.onmessage = function(e) { var
reader = new FileReader(); reader.onload = function(event) { var
contents = event.target.result; var a = new Image(); a.src = contents;
document.body.appendChild(a); } reader.readAsDataU途睿欧L(e.data); };

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var ws = new WebSocket("ws://xxx.xxx.xxx.xxx:8888");
 
ws.onopen = function(){
    console.log("握手成功");
};
 
ws.onmessage = function(e) {
    var reader = new FileReader();
    reader.onload = function(event) {
        var contents = event.target.result;
        var a = new Image();
        a.src = contents;
        document.body.appendChild(a);
    }
    reader.readAsDataURL(e.data);
};

接收到音讯,然后readAsDataU奥迪Q5L,直接将图纸base64加多到页面中

转到服务器端代码

JavaScript

fs.readdir(“skyland”, function(err, files) { if(err) { throw err; }
for(var i = 0; i < files.length; i++) { fs.readFile(‘skyland/’ +
files[i], function(err, data) { if(err) { throw err; }
o.write(encodeImgFrame(data)); }); } }); function encodeImgFrame(buf) {
var s = [], l = buf.length, ret = []; s.push((1 << 7) + 2);
if(l < 126) { s.push(l); } else if(l < 0x10000) { s.push(126,
(l&0xFF00) >> 8, l&0xFF); } else { s.push(127, 0, 0, 0, 0,
(l&0xFF000000) >> 24, (l&0xFF0000) >> 16, (l&0xFF00)
>> 8, l&0xFF); } return Buffer.concat([new Buffer(s), buf]); }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
fs.readdir("skyland", function(err, files) {
if(err) {
throw err;
}
for(var i = 0; i < files.length; i++) {
fs.readFile(‘skyland/’ + files[i], function(err, data) {
if(err) {
throw err;
}
 
o.write(encodeImgFrame(data));
});
}
});
 
function encodeImgFrame(buf) {
var s = [],
l = buf.length,
ret = [];
 
s.push((1 << 7) + 2);
 
if(l < 126) {
s.push(l);
} else if(l < 0x10000) {
s.push(126, (l&0xFF00) >> 8, l&0xFF);
} else {
s.push(127, 0, 0, 0, 0, (l&0xFF000000) >> 24, (l&0xFF0000) >> 16, (l&0xFF00) >> 8, l&0xFF);
}
 
return Buffer.concat([new Buffer(s), buf]);
}

注意s.push((1 << 7) +
2)
这一句,这里格外直接把opcode写死了为2,对于Binary
Frame,那样顾客端接收到数码是不会尝试进行toString的,不然会报错~

代码非常粗大略,在那边向我们大饱眼福一下websocket传输图片的速度怎么着

测验非常多张图片,总共8.24M

平时静态能源服务器要求20s左右(服务器较远)

cdn需要2.8s左右

那大家的websocket格局吧??!

答案是千篇一律须要20s左右,是还是不是很失望……速度正是慢在传输上,并非服务器读取图片,本机上平等的图形财富,1s左右得以成功……那样看来数据流也无力回天冲破距离的界定升高传输速度

上边我们来探视websocket的另一个用法~

 

用websocket搭建语音聊天室

先来收拾一下口音聊天室的功用

顾客步入频道随后从迈克风输入音频,然后发送给后台转载给频道里面包车型客车其余人,其余人接收到新闻举办播放

看起来困难在八个地点,第2个是节奏的输入,第二是吸取到数量流实行广播

先说音频的输入,这里运用了HTML5的getUserMedia方法,可是注意了,本条点子上线是有葵涌的,最终说,先贴代码

JavaScript

if (navigator.getUserMedia) { navigator.getUserMedia( { audio: true },
function (stream) { var rec = new SRecorder(stream); recorder = rec; })
}

1
2
3
4
5
6
7
8
if (navigator.getUserMedia) {
    navigator.getUserMedia(
        { audio: true },
        function (stream) {
            var rec = new SRecorder(stream);
            recorder = rec;
        })
}

首先个参数是{audio:
true},只启用音频,然后创立了三个SRecorder对象,后续的操作基本上都在那么些目的上开展。此时只要代码运转在本土的话浏览器应该提醒您是不是启用Mike风输入,显著现在就运行了

接下去大家看下SRecorder构造函数是什么,给出主要的一部分

JavaScript

var SRecorder = function(stream) { …… var context = new AudioContext();
var audioInput = context.createMediaStreamSource(stream); var recorder =
context.createScriptProcessor(4096, 1, 1); …… }

1
2
3
4
5
6
7
var SRecorder = function(stream) {
    ……
   var context = new AudioContext();
    var audioInput = context.createMediaStreamSource(stream);
    var recorder = context.createScriptProcessor(4096, 1, 1);
    ……
}

奥迪oContext是三个节奏上下文对象,有做过声音过滤管理的同学应该知道“一段音频达到扬声器进行播放以前,半路对其进展阻挠,于是大家就赢得了节奏数据了,这些拦截专门的工作是由window.奥迪oContext来做的,大家全体对旋律的操作都根据那几个目的”,我们得以经过奥迪(Audi)oContext创制差异的奥迪oNode节点,然后增加滤镜播放非常的音响

录音原理同样,我们也急需走奥迪(Audi)oContext,可是多了一步对迈克风音频输入的接收上,而不是像未来管理音频一下用ajax央浼音频的ArrayBuffer对象再decode,Mike风的承受需求用到createMediaStreamSource方法,注意这几个参数就是getUserMedia方法第二个参数的参数

并且createScriptProcessor方法,它官方的演说是:

Creates a ScriptProcessorNode, which can be used for direct audio
processing via JavaScript.

——————

席卷下正是其一措施是行使JavaScript去管理音频搜集操作

到底到点子采撷了!胜利就在前边!

接下去让我们把话筒的输入和节奏收罗相连起来

JavaScript

audioInput.connect(recorder); recorder.connect(context.destination);

1
2
audioInput.connect(recorder);
recorder.connect(context.destination);

context.destination官方解释如下

The destination property of
the AudioContext interface
returns
an AudioDestinationNoderepresenting
the final destination of all audio in the context.

——————

context.destination重临代表在意况中的音频的末梢指标地。

好,到了此时,大家还亟需贰个监听音频采撷的事件

JavaScript

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

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

audioData是一个目的,那一个是在网络找的,作者就加了贰个clear方法因为后边会用到,首要有非常encodeWAV方法非常的赞,外人进行了累累的音频压缩和优化,那么些最终会伴随完整的代码一同贴出来

此时总体客商进入频道随后从迈克风输入音频环节就曾经变成啦,上边就该是向服务器端发送音频流,稍微有一点点蛋疼的来了,刚才大家说了,websocket通过opcode不一样能够象征回去的数目是文本依然二进制数据,而大家onaudioprocess中input进去的是数组,最后播放音响供给的是Blob,{type:
‘audio/wav’}的目的,那样大家就务要求在发送在此之前将数组调换到WAV的Blob,此时就用到了上边说的encodeWAV方法

服务器就如很简短,只要转载就行了

地方测验确实能够,可是天坑来了!将前后相继跑在服务器上时候调用getUserMedia方法提醒小编不能够不在三个平安的条件,也正是索要https,那表示ws也必需换到wss……所以服务器代码就未有利用大家团结包裹的拉手、分析和编码了,代码如下

JavaScript

var https = require(‘https’); var fs = require(‘fs’); var ws =
require(‘ws’); var userMap = Object.create(null); var options = { key:
fs.readFileSync(‘./privatekey.pem’), cert:
fs.readFileSync(‘./certificate.pem’) }; var server =
https.createServer(options, function(req, res) { res.writeHead({
‘Content-Type’ : ‘text/html’ }); fs.readFile(‘./testaudio.html’,
function(err, data) { if(err) { return ; } res.end(data); }); }); var
wss = new ws.Server({server: server}); wss.on(‘connection’, function(o)
{ o.on(‘message’, function(message) { if(message.indexOf(‘user’) === 0)
{ var user = message.split(‘:’)[1]; userMap[user] = o; } else {
for(var u in userMap) { userMap[u].send(message); } } }); });
server.listen(8888);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
var https = require(‘https’);
var fs = require(‘fs’);
var ws = require(‘ws’);
var userMap = Object.create(null);
var options = {
    key: fs.readFileSync(‘./privatekey.pem’),
    cert: fs.readFileSync(‘./certificate.pem’)
};
var server = https.createServer(options, function(req, res) {
    res.writeHead({
        ‘Content-Type’ : ‘text/html’
    });
 
    fs.readFile(‘./testaudio.html’, function(err, data) {
        if(err) {
            return ;
        }
 
        res.end(data);
    });
});
 
var wss = new ws.Server({server: server});
 
wss.on(‘connection’, function(o) {
    o.on(‘message’, function(message) {
if(message.indexOf(‘user’) === 0) {
    var user = message.split(‘:’)[1];
    userMap[user] = o;
} else {
    for(var u in userMap) {
userMap[u].send(message);
    }
}
    });
});
 
server.listen(8888);

代码照旧极粗略的,使用https模块,然后用了起来讲的ws模块,userMap是模拟的频段,只兑现转发的着力职能

运用ws模块是因为它非常https完成wss实在是太平价了,和逻辑代码0抵触

https的搭建在那边就不提了,首假使索要私钥、CSR证书具名和证件文件,感兴趣的同室能够精通下(不过不打听的话在现网遇到也用持续getUserMedia……)

下边是全部的前端代码

JavaScript

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(‘请输入客户名’);
return false; } if(!navigator.getUserMedia) {
alert(‘抱歉您的装置无斯洛伐克(Slovak)语音聊天’); 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; config.sampleRate = config.sampleRate || (44100
/ 6); var context = new 奥迪(Audi)oContext(); var audioInput =
context.createMediaStreamSource(stream); var recorder =
context.createScriptProcessor(4096, 1, 1); var audioData = { size: 0
//录音文件长度 , buffer: [] //录音缓存 , inputSampleRate:
context.sampleRate //输入采集样品率 , inputSampleBits: 16 //输入采集样品数位 8,
16 , outputSampleRate: config.sampleRate //输出采集样品率 , oututSampleBits:
config.sampleBits //输出采集样品数位 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)); } }; // 财富沟通文件标志符
writeString(‘ENVISIONIFF’); 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 (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); }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
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(‘请输入用户名’);
        return false;
    }
    if(!navigator.getUserMedia) {
        alert(‘抱歉您的设备无法语音聊天’);
        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;
    config.sampleRate = config.sampleRate || (44100 / 6);
 
    var context = new AudioContext();
    var audioInput = context.createMediaStreamSource(stream);
    var recorder = context.createScriptProcessor(4096, 1, 1);
 
    var audioData = {
        size: 0          //录音文件长度
        , buffer: []     //录音缓存
        , inputSampleRate: context.sampleRate    //输入采样率
        , inputSampleBits: 16       //输入采样数位 8, 16
        , outputSampleRate: config.sampleRate    //输出采样率
        , oututSampleBits: config.sampleBits       //输出采样数位 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));
                }
            };
 
            // 资源交换文件标识符
            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 (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);
}

注意:按住a键说话,放开a键发送

本身有品味不开关实时对讲,通过setInterval发送,但意识杂音有一点点重,效果不佳,那几个必要encodeWAV再一层的卷入,多去除蒙受杂音的效果,自个儿选取了尤其方便的开关说话的情势

 

那篇小说里首先展望了websocket的今后,然后遵照专门的工作我们温馨尝试深入分析和转移数据帧,对websocket有了更加深一步的垂询

最终经过三个demo看到了websocket的潜质,关于语音聊天室的demo涉及的较广,未有接触过奥迪oContext对象的同校最棒先明白下奥迪(Audi)oContext

小提起此地就截至啦~有何样主张和难点款待大家提议来一同商讨探求~

 

1 赞 11 收藏 3
评论

图片 6

相关文章

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图