搬砖: web音频流转发之音视频直播

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进制原始音频数据进行播放

采集和推流

  1. 获取摄像头,和麦克风需要https
  2. 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);
        };
    }

注意

  1. 这只是一个实例程序,为进行任何优化
  2. 在测试时请给扬声器插上耳机收听,或者让扬声器和麦克风放置到不同的房间。因为没有做回音消除,和破音处理,这样听上去会很爽。
  3. 自己生成一个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);
            }
          });
      }); 
    });
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值