web音频流转发之音频源
2.1k 次阅读 · 读完需要 16 分钟
前言
web音频流转发之音视频直播
web音频流转发之AudioNode
app能直播,web为什么不可以?看完本系列文章,你就能做一个直播,真正的直播,包括音频流的转发,这也是我最近查看web audio api发现有相关api能实现音频流的转发,所有打算分享系列文章供大家交流,如有不对之处请指正。看完本系列文章能完成一个web直播,当然包括视频流。当然webrtc也能实现web直播,但是实现原理有所差别。文章也很简单就是对web audio api的灵活使用。
兼容性
兼容性是有必要看下,顺便看一下ie,和安卓Android Browse的兼容性,然而我想说我的荣耀8 安卓6.0 还是嵌套的chrome 37你们信吗?到底是作为一个渐渐增强的功能,还是作为一个装逼技能就看大家的使用啦!
概述
一个简单而典型的web audio流程如下:
1.创建音频上下文
2.在音频上下文里创建源 — 例如 <audio>, 振荡器, 流
3.创建效果节点,例如混响、双二阶滤波器、平移、压缩
4.为音频选择一个目地,例如你的系统扬声器
5.连接源到效果器,对目的地进行效果输出
这段话引用自MDN,这一章我主要讲解inputs中的3个api。
createMediaElementSource
下面是一个简单到极点的例子,完成了从audio获取音频源,然后输出到你的扬声器设备。
这个方法我们在直播中做用不到。
//创建音频上下文
let audioCtx = new (window.AudioContext || window.webkitAudioContext)(),
//获取audio节点
myAudio = document.querySelector('audio'),
//创建音频源
source = audioCtx.createMediaElementSource(myAudio),
//将音频源直接连接到输出设备
source.connect(audioCtx.destination);
createMediaStreamSource
这个方法介绍的是用navigator.mediaDevices.getUserMedia(navigator.getUserMedia已经废弃)获取到的音频流作为音频流。在直播时我们会采用此方法作为,音频流的采集方法;当然在这里我们也会获取到我们需要的视频流数据
//简单兼容
let getUserMedia = navigator.mediaDevices.getUserMedia || navigator.getUserMedia;
//获取麦克风,摄像头权限
getUserMedia({audio: true, video: true}).then(stream => {
let audioCtx = new AudioContext(),
//以流媒体作为音频源
source = audioCtx.createMediaStreamSource(stream);
//将音频源直接连接到输出设备
source.connect(audioCtx.destination);
});
createBufferSource
这个方法相对前面两个稍微复杂一点点。createBufferSource是由存储器中的音频数据组成的音频源,它通过AudioBuffer来进行存储,解释一下:它是通过一个固定的音频数据的二进制作为音频源,比如一首歌的二进制数据。
let audioCtx = new (window.AudioContext || window.webkitAudioContext)(),
source = audioCtx.createBufferSource();
//myArrayBuffer是一个AudioBuffer
source.buffer = myArrayBuffer;
source.loop = true; //循环播放
source.connect(audioCtx.destination);
source.start(); //开始播放音频源
下面讲讲myArrayBuffer的几种来源方式
//1.通过input=file 获取的音频文件
let fileInput = document.querySelector('input'),
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
fileInput.onchange = function(ev){
let file = ev.target.files[0],
fr = new FileReader();
fr.readAsArrayBuffer(file);
fr.onload = function(data){
//result是一个arraybuffer类型二进制数据
let result = data.target.result;
//解析数据
analyticBuffer(result);
};
};
//2.通过XHR获取音频数据(注意需要返回arraybuffer类型)
let request = new XMLHttpRequest();
request.open('GET', 'xxx.mp3', true);
//指定文件返回数据类型
request.responseType = 'arraybuffer';
//请求成功时执行
request.onload = function() {
//这是一个arraybuffer
var buffer = request.response;
//解析数据
analyticBuffer(buffer )
}
request.send();
//解析二进制数据
function analyticBuffer(buffer){
//将ArrayBuffer异步转换为一个AudioBuffer类型
audioCtx.decodeAudioData(buffer, (myArrayBuffer) => {
let source = audioCtx.createBufferSource();
source.buffer = myArrayBuffer;
source.connect(audioCtx.destination);
source.start();
});
}
//3.自己创造一个AudioBuffer
//采样率sample/s
let sampleRate = audioCtx.sampleRate,
//帧数,音频时间 = frameCount / sampleRate
frameCount = audioCtx.sampleRate * 2.0,
//创建一个两通道的音频数据,这是一个没有声音的音频数据
myArrayBuffer = audioCtx.createBuffer(2, frameCount , sampleRate);
//随机填充白噪音
//两个通道循环2次
for (var channel = 0; channel < 2; channel++) {
//获取每个通道的array数据
var nowBuffering = myArrayBuffer.getChannelData(channel);
for (let i = 0; i < frameCount; i++) {
//对每一帧填充数据
nowBuffering[i] = Math.random() * 2 - 1;
}
}
AudioBuffer的属性和方法
AudioBuffer的方法在我们直播的时候需要用到,在后面的AudioNode(音频处理模块)中也会出现AudioBuffer数据,我们需要它是获取和传输数据
let myArrayBuffer = audioCtx.createBuffer(2, 4096, sampleRate);
myArrayBuffer.sampleRate //采样数
myArrayBuffer.length //采样帧率 也就是4096
myArrayBuffer.duration //时长
myArrayBuffer.numberOfChannels //通道数
//返回x通道的Float32Array类型的数据,x表示是哪个通道0或1
myArrayBuffer.getChannelData(x)
//将myArrayBuffer第x通道的数据复制到anotherArray中,y表示数据复制开始的偏移量
let anotherArray = new Float32Array;
myArrayBuffer.copyFromChannel(anotherArray,x,y);
//将anotherArray数据复制到myArrayBuffer的X通道中,y偏移量
let anotherArray = new Float32Array;
myArrayBuffer.copyToChannel(anotherArray,x,y);
//关于copyToChannel,copyFromChannel,getChannelData在下一章看见例子就明白了
结束语
这一章大概就说到这么多吧,都是一些简单的api使用,下一章结合以后例子讲解部分AudioNode。
web音频流转发之AudioNode
1.8k 次阅读 · 读完需要 23 分钟
前言
上一章地址: web音频流转发之音频源
下一张地址:web音频流转发之音视频直播
在这一章我说几个我们需要用到的音频处理模块也就3个吧,包括我们转发流是需要用到的核心模块。更多模块请看MDN,或者看HTML5音频API Web Audio也有一些中文讲解,希望大家多多支持。
概述
AudioNode:是一个处理音频的通用模块, 比如一个音频源 (e.g. 一个 HTML <audio> or <video> 元素), 一个音频地址或者一个中间处理模块 (e.g. 一个过滤器如 BiquadFilterNode, 或一个音量控制器如 GainNode).一个AudioNode 既有输入也有输出。输入与输出都有一定数量的通道。只有一个输出而没有输入的 AudioNode 叫做音频源(MDN的解释)。下面我用简单的代码给大家解释下
let audioCtx = new window.AudioContext,
//频率及时间域分析器,声音可视化就用这个
analyser = audioCtx.createAnalyser(),
//音量变更模块
gainNode = audioCtx.createGain(),
//波形控制器模块
distortion = audioCtx.createWaveShaper(),
//低频滤波器模块
biquadFilter = audioCtx.createBiquadFilter(),
//创建源
source = audioCtx.createMediaStreamSource(stream);
//通过connect方法从音频源连接到AudioNode处理模块,再连接到输出设备,
//当我们修改前面的模块时,就会影响到后面AudioNode,以及我们最终听见的声音
source.connect(analyser);
analyser.connect(distortion);
distortion.connect(biquadFilter);
biquadFilter.connect(convolver);
convolver.connect(gainNode);
gainNode.connect(audioCtx.destination);
下面我就分别讲解我们需要用的几个api
createAnalyser
下面简单说一下它的方法和属性,具体的使用在后面的demo中,说实话这些属性我也不知道有有什么用,但是我们能通过方法取到我们需要的数据,做音频可视化。analyser中的数据会根据数据的不同会不停的变换,所有我们需要用requestAnimationFrame函数,反复获取里面的数据,然后进行绘图。
let analyser = audioCtx.createAnalyser();
//频域的FFT大小,默认是2048
analyser.fftSize;
//fftSize的一半
analyser.frequencyBinCount;
//快速傅立叶变化的最大范围的双精度浮点数
analyser.maxDecibels;
//最后一个分析帧的平均常数
analyser.smoothingTimeConstant;
//将当前频域数据拷贝进Float32Array数组
analyser.getFloatFrequencyData()
//将当前频域数据拷贝进Uint8Array数组
analyser.getByteFrequencyData()
将当前波形,或者时域数据拷贝进Float32Array数组
analyser.getFloatTimeDomainData()
//将当前波形,或者时域数据拷贝进 Uint8Array数组
analyser.getByteTimeDomainData()
createGain
这个简单到极点。。。
let gainNode = audioCtx.createGain();
//修改value的大小,改变输出大小,默认是1,0表示静音
gainNode.gain.value = 1
createScriptProcessor
缓冲区音频处理模块,这个是我们做直播的核心模块,没有这个模块就做不到音频流的转发,音频数据的延迟在除开网络的影响下,这个模块也占一部分,当然要看自己的配置。AudioBuffer介绍
/*
第一个参数表示每一帧缓存的数据大小,可以是256, 512, 1024, 2048, 4096, 8192, 16384,
值越小一帧的数据就越小,声音就越短,onaudioprocess 触发就越频繁。
4096的数据大小大概是0.085s,就是说每过0.085s就触发一次onaudioprocess,
如果我要把这一帧的数据发送到其他地方,那这个延迟最少就是0.085s,
当然还有考虑发送过去的电脑处理能力,一般1024以上的数字,如果有变态需求的256也是可以考虑的
第二,三个参数表示输入帧,和输出帧的通道数。这里表示2通道的输入和输出,当然我也可以采集1,4,5等通道
*/
let recorder = audioCtx.createScriptProcessor(4096, 2, 2);
/*
缓存区触发事件,连接了createScriptProcessor这个AudioNode就需要在onaudioprocess中,
把输入帧的数据,连接到输出帧,扬声器才有声音
*/
recorder.onaudioprocess = function(e){
let inputBuffer = e.inputBuffer, //输入帧数据,AudioBuffer类型
outputBuffer = e.outputBuffer; //输出帧数据, AudioBuffer类型
//第一种方式
//将inputBuffer第0个通道的数据,复制到outputBuffer的第0个通道,偏移0个字节
outputBuffer.copyToChannel(inputBuffer.getChannelData(0), 0, 0);
//将inputBuffer第1个通道的数据,复制到outputBuffer的第1个通道,偏移0个字节
outputBuffer.copyToChannel(inputBuffer.getChannelData(1), 1, 0);
//第二中方式用循环
for (var channel = 0; channel < outputBuffer.numberOfChannels; channel++) {
let inputData = inputBuffer.getChannelData(channel),
outputData = outputBuffer.getChannelData(channel);
for (var sample = 0; sample < inputBuffer.length; sample++) {
outputData[sample] = inputData[sample];
}
}
}
举个栗子
下面我用input=file选择一个本地音乐举个栗子,采用哪种音频看自己哟
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<link rel="stylesheet" href="">
</head>
<body>
<canvas width="500px" height="500px">
</canvas>
<input type="file" name="" value="" placeholder="">
<button type="button" class="add">音量+</button>
<button type="button" class="lost">音量-</button>
</body>
<script type="text/javascript" charset="utf-8">
let fileInput = document.querySelector('input'),
add = document.querySelector('.add'), //音量+
lost = document.querySelector('.lost'), //音量-
audioCtx = new window.AudioContext, //创建环境
analyser = audioCtx.createAnalyser(), //analyser分析器
gainNode = audioCtx.createGain(), //控制音量大小
recorder = audioCtx.createScriptProcessor(4096, 2, 2), //缓冲区音频处理模块
canvas = document.querySelector('canvas'),
canvasCtx = canvas.getContext('2d');
fileInput.onchange = function(ev){
let file = ev.target.files[0],
fr = new FileReader();
fr.readAsArrayBuffer(file);
fr.onload = function(data){
let result = data.target.result;
//解码ArrayBuffer
audioCtx.decodeAudioData(result, getBuffer);
};
};
//修改音量大小
add.onclick = function(){
gainNode.gain.value += 0.1;
};
lost.onclick = function(){
gainNode.gain.value -= 0.1;
}
function getBuffer(audioBuffer){
//创建对象,用过AudioBuffer对象来播放音频数据
let source = audioCtx.createBufferSource();
source.buffer = audioBuffer;
//将source与analyser分析器连接
source.connect(analyser);
//将analyser与gainNode分析器连接
analyser.connect(gainNode);
//音量控制器与输出设备链接
gainNode.connect(recorder);
recorder.connect(audioCtx.destination);
//播放
source.start(0);
draw(analyser);
//音频采集
recorder.onaudioprocess = function (e) {
/*输入流,必须要链接到输出流,audioCtx.destination才能有输出*/
let inputBuffer = e.inputBuffer, outputBuffer = e.outputBuffer;
outputBuffer.copyToChannel(inputBuffer.getChannelData(0), 0, 0);
outputBuffer.copyToChannel(inputBuffer.getChannelData(1), 1, 0);
};
}
let WIDTH = 500, HEIGHT = 500;
//绘制波形图
function draw() {
requestAnimationFrame(draw);
//保存频率数据
let dataArray = new Uint8Array(analyser.fftSize),
bufferLength = analyser.fftSize;
//获取频域的输出信息
analyser.getByteTimeDomainData(dataArray);
canvasCtx.fillStyle = 'rgb(200, 200, 200)';
canvasCtx.fillRect(0, 0, 500, 500);
canvasCtx.lineWidth = 2;
canvasCtx.strokeStyle = 'rgb(0, 0, 0)';
canvasCtx.beginPath();
var sliceWidth = WIDTH * 1.0 / bufferLength;
var x = 0;
for(var i = 0; i < bufferLength; i++) {
var v = dataArray[i] / 128.0;
var y = v * HEIGHT/2;
if(i === 0) {
canvasCtx.moveTo(x, y);
} else {
canvasCtx.lineTo(x, y);
}
x += sliceWidth;
}
canvasCtx.lineTo(canvas.width, canvas.height/2);
canvasCtx.stroke();
};
</script>
</html>
结束语
基本上我们要用到的api都介绍完了,我想大家应该知道如何做音频转发了吧。下面一章就开始介绍音频流的转发了。
HTML5音频API Web Audio这一篇文章还是可以看看的。
希望大家多多支持,收藏点赞呀
web音频流转发之音视频直播
前言
经过前面两篇文章的讲解,大家已经了解了audio的基本使用方法,下面我们就根据我们了解的api做一个直播。
web音频流转发之AudioNode
web音频流转发之音频源
原理
- 视频直播:采集一帧一帧的视频,转换为base64转发,接收到base64后,设置为img的src,然后不停的修改img的src形成视频
- 音频直播:采集一帧一帧的音频二进制数据,转发2进制数据,在接收端对2进制原始音频数据进行播放
采集和推流
- 获取摄像头,和麦克风需要https
- navigator.getUserMedia已经废弃,使用navigator.mediaDevices.getUserMedia,当然需要做兼容
//获取音频视频流数据
mediaDevices = navigator.mediaDevices.getUserMedia({audio: true,video: { width: 320, height: 240 }});
mediaDevices.then(stream => {
//视频流转换到video标签播放
video.srcObject = stream;
video.play();
//音频流转换到AudioNode做数据采集
let source = audioCtx.createMediaStreamSource(stream);
recorder = audioCtx.createScriptProcessor(2048, 1, 1);
source.connect(recorder);
recorder.connect(audioCtx.destination);
recorder.onaudioprocess = function(ev){
//采集单声道数据
let inputBuffer = ev.inputBuffer.getChannelData(0);
//将视频画面转换成base64发送
ws.send(canvas.toDataURL('image/jpeg'));
//发送音频pcm数据
ws.send(inputBuffer.buffer);
};
});
video.onplay = function(){
//将video绘制到canvas上
interval = setInterval(function(){
ctx.drawImage(video, 0, 0);
},30);
};
接收流文件
对接收的文件进行一个缓存,以达到一个好的用户体验
let ws = new WebSocket("wss://192.168.3.102"),
imgChuncks = [],
audioChuncks = [],
img = null;
//如何处理二进制数据,默认是Blob
ws.binaryType = 'arraybuffer',
ws.onmessage = function(evt) {
if(evt.data.byteLength === undefined) {
//收到的base64图片
imgChuncks.push(evt.data);
}else{
//收到的音频二进制pcm数据
audioChuncks.push(new Float32Array(evt.data));
}
//缓存2帧的数据后开始播放
if(!img && audioChuncks.length > 2){
myplay();
}
};
处理流
//创建播放音频视频函数
function myplay(){
//创建img标签来播放base64图片
img = new Image();
document.body.appendChild(img);
//创建播放音频对象
let myBuffer = audioCtx.createBuffer(1, 2048, audioCtx.sampleRate),
source = audioCtx.createBufferSource(),
recorder = audioCtx.createScriptProcessor(2048, 1, 1);
source.connect(recorder);
recorder.connect(audioCtx.destination);
recorder.onaudioprocess = function(ev){
//修改img的src达到视频的效果
img.src = imgChuncks.shift();
//播放audioChuncks里面真正的二进制数据
ev.outputBuffer.copyToChannel(audioChuncks.shift() || new Float32Array(2048), 0, 0);
};
}
注意
- 这只是一个实例程序,为进行任何优化
- 在测试时请给扬声器插上耳机收听,或者让扬声器和麦克风放置到不同的房间。因为没有做回音消除,和破音处理,这样听上去会很爽。
- 自己生成一个https文件做测试
完整代码
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<link rel="stylesheet" href="">
<style type="text/css" media="screen">
video, canvas {
background-color: #e9e9e9;
margin:0 auto;
display: block;
}
body {
text-align: center;
}
video {
display: none;
}
</style>
</head>
<body>
<canvas width="320px" height="240px">
</canvas>
<video src="" width="320px" height="240px" controls muted></video>
<button type="button" class="start">开始</button>
</body>
<script type="text/javascript">
let ws = new WebSocket("wss://192.168.3.102"),
imgChuncks = [],
audioChuncks = [],
img = null;
//如何处理二进制数据,默认是Blob
ws.binaryType = 'arraybuffer',
ws.onmessage = function(evt) {
if(evt.data.byteLength === undefined) {
//收到的base64图片
imgChuncks.push(evt.data);
}else{
//收到的音频二进制pcm数据
audioChuncks.push(new Float32Array(evt.data));
}
//缓存2帧的数据后开始播放
if(!img && audioChuncks.length > 2){
myplay();
}
};
//创建播放音频视频函数
function myplay(){
//创建img标签来播放base64图片
img = new Image();
document.body.appendChild(img);
//创建播放音频对象
let myBuffer = audioCtx.createBuffer(1, 2048, audioCtx.sampleRate),
source = audioCtx.createBufferSource(),
recorder = audioCtx.createScriptProcessor(2048, 1, 1);
source.connect(recorder);
recorder.connect(audioCtx.destination);
recorder.onaudioprocess = function(ev){
//修改img的src达到视频的效果
img.src = imgChuncks.shift();
//播放audioChuncks里面真正的二进制数据
ev.outputBuffer.copyToChannel(audioChuncks.shift() || new Float32Array(2048), 0, 0);
};
}
let video = document.querySelector('video'),
start = document.querySelector('.start'),
stop = document.querySelector('.stop'),
canvas = document.querySelector('canvas'),
ctx = canvas.getContext('2d'),
audioCtx = new (window.AudioContext || window.webkitAudioContext)(),
interval = null,
mediaDevices = null;
//点击开始
start.onclick = function(){
//获取音频视频流数据
mediaDevices = navigator.mediaDevices.getUserMedia({audio: true,video: { width: 320, height: 240 }});
mediaDevices.then(stream => {
//视频流转换到video标签播放
video.srcObject = stream;
video.play();
//音频流转换到AudioNode做数据采集
let source = audioCtx.createMediaStreamSource(stream);
recorder = audioCtx.createScriptProcessor(2048, 1, 1);
source.connect(recorder);
recorder.connect(audioCtx.destination);
recorder.onaudioprocess = function(ev){
//采集单声道数据
let inputBuffer = ev.inputBuffer.getChannelData(0);
//将视频画面转换成base64发送
ws.send(canvas.toDataURL('image/jpeg'));
//发送音频pcm数据
ws.send(inputBuffer.buffer);
};
});
};
video.onplay = function(){
//将video绘制到canvas上
interval = setInterval(function(){
ctx.drawImage(video, 0, 0);
},30);
};
</script>
</html>
servers.js
let https = require('https'),
fs = require('fs'),
WebSocket = require('ws'),
options = {
key: fs.readFileSync('./key.pem'),
cert:fs.readFileSync('./key-cert.pem')
},
server = https.createServer(options, function(req, res){
fs.readFile('./index.html', function(err, data){
res.writeHead(200,{'Content-Type': 'text/html'});
res.end(data);
});
}).listen(443, function(){
console.log('服务启动成功')
});
const wss = new WebSocket.Server({server});
wss.binaryType = 'arraybuffer';
wss.on('connection', (ws) => {
ws.on('message', function(data) {
wss.clients.forEach(function each(client) {
if (client.readyState === WebSocket.OPEN && client !== ws) {
client.send(data);
}
});
});
});