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

作者:ca88

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中的源码图

ca88 1

ca88 2

 

3、ws模块

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

 

二、自身达成豆蔻年华套server端websocket

正巧说了三种普及的websocket达成模式,今后我们思考,对于开荒者来讲

websocket相对于守旧http数据交互作用形式以来,扩张了服务器推送的风浪,顾客端选取到事件再扩充对应管理,开垦起来区别并非太大啊

这是因为那么些模块已经帮咱们将数码帧深入分析此间的坑都填好了,第二片段我们将尝试本身创制生龙活虎套简便的服务器端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");

ca88 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 Protocolsrn'); o.write('Upgrade: websocketrn'); o.write('Connection: Upgradern'); // 这么些字段带上服务器管理后的KEY o.write('Sec-WebSocket-Accept: ' key 'rn'); // 输出空行,使HTTP头甘休 o.write('rn'); } }); }).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 Protocolsrn');
o.write('Upgrade: websocketrn');
o.write('Connection: Upgradern');
// 这个字段带上服务器处理后的KEY
o.write('Sec-WebSocket-Accept: ' key 'rn');
// 输出空行,使HTTP头结束
o.write('rn');
}
});
}).listen(8888);

那般握手部分就早就实现了,前边正是多少帧拆解深入分析与变化的活了

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

ca88 4

简短介绍下

FIN为是不是终止的标识

OdysseySV为留下空间,0

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

交给一张opcode对应图

ca88 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)

  • 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; }
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、传输图片

我们先讨论传输图片的步骤是如何,首先服务器收到到顾客端央求,然后读取图片文件,将二进制数据转载给客商端,客商端如哪个地方理?当然是运用FileReader对象了

先给顾客端代码

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.readAsDataUSportageL(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);
};

收到到新闻,然后readAsDataULacrosseL,直接将图片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的,否则会报错ca88,~

代码非常粗略,在这地向大家慌不择路一下websocket传输图片的快慢怎样

测量试验相当多张图片,总共8.24M

日常性静态财富服务器要求20s左右(服务器较远卡塔 尔(英语:State of Qatar)

cdn需要2.8s左右

那我们的websocket方式吗??!

答案是生龙活虎致需求20s左右,是否相当大失所望……速度正是慢在传输上,并不是服务器读取图片,本机上亦然的图纸财富,1s左右能够完结……那样看来数据流也无计可施冲破间隔的限量升高传输速度

上面大家来会见websocket的另贰个用法~

 

用websocket搭建语音闲谈室

先来收拾一下口音闲话室的功效

客户步入频道随后从迈克风输入音频,然后发送给后台转载给频道里面包车型大巴别的人,别的人接纳到消息实行广播

看起来困难在四个地点,第多个是节奏的输入,第二是接到到数码流进行播报

先说音频的输入,这里运用了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对象,后续的操作基本上都在这里个目的上实行。那时候即使代码运行在该地的话浏览器应该晋升您是还是不是启用迈克风输入,明确之后就运营了

接下去大家看下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);
    ……
}

奥迪(Audi卡塔尔oContext是三个旋律上下文对象,有做过声音过滤管理的校友应该驾驭“生龙活虎段音频抵达扬声器实行播放在此以前,半路对其开展拦阻,于是大家就收获了旋律数据了,这么些拦截专门的学问是由window.奥迪oContext来做的,大家具有对旋律的操作都依据那些目的”,大家得以经过奥迪(Audi卡塔 尔(英语:State of Qatar)oContext创设不相同的奥迪oNode节点,然后增加滤镜播放特其他音响

录音原理同样,大家也亟需走奥迪(Audi卡塔 尔(阿拉伯语:قطر‎oContext,不过多了一步对Mike风音频输入的选拔上,并不是像往常管理音频一下用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方法非常的赞,外人实行了累累的点子压缩和优化,那个最后会陪伴完整的代码一同贴出来

那会儿整整客商步入频道随后从Mike风输入音频环节就曾经成功啦,上面就该是向劳动器端发送音频流,微微有一点点蛋疼的来了,刚才大家说了,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的搭建在那就不提了,首倘若索要私钥、CS奥迪Q5证书签字和证件文件,感兴趣的校友能够理解下(不过不打听的话在现网意况也用持续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('抱歉您的配备无菲律宾语音闲聊'); 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 //输出采样率 , outut萨姆pleBits: 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('中华VIFF'); 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对象的同校最棒先了然下奥迪oContext

作品到此处就命丧黄泉啦~有如何主张和主题材料接待大家提议来一同谈谈索求~

 

1 赞 11 收藏 3 评论

ca88 6

本文由ca88发布,转载请注明来源

关键词: ca88网址 javascript ca888亚洲娱乐城