TRTC Web端 仿腾讯会议麦克风静音检测

2021-04-27 22:52:53 浏览数 (1)

项目背景

目前 Web 的 TRTC 没有静音检测,在关闭麦克风的情况下发言没有提示,有时候会有比较尴尬的会议场景出现,为提升用户体验,这里尝试将腾讯会议的解决思路引入。

当用户在关闭麦克风的情况下,如果周围声音超过一定的分贝值,则显示提示。

目前可兼容主流PC浏览器下的所有框架,electron框架,为更好接入TRTC Web Demo 采用jquery库开发(主要是交互与样式),稍微改动即可适配原生与其他框架。

这份代码直接在TRTC的Demo中写一份js文件引入即可,即插即用。如果有需要自定义的部分,我也在代码里写了注释。

使用方法:

image.pngimage.png

注:该功能需要在服务端才可使用,调试请使用Live Server。

效果展示:

image.pngimage.png

源代码

说起来很简单,实际做起来有些难度,根本上是需要用js去加工音频流才行,用到了一个很旧的接口audioContext以及createMediaStreamSource相关的方法才得以实现,这里直接贴结果代码。

代码语言:txt复制
var audioContext = null;
var meter = null;
// 如果你需要对声音大小可视化,可以将canvasContext解除注释,并在html里写一个canvas元素供给后续获取
// var canvasContext = null;
var mediaStreamSource = null;
var WIDTH = 500;
var HEIGHT = 50;
var rafID = null;
var hintlock = false;

// jquery,如果需兼容其他框架,可以改用原生
$('body').append(`   <div id="michint" class="alert alert-warning">
    <strong>注意!</strong>检测到您说话但没有打开麦克风!。
</div>`)
$('#michint').slideUp(0)

getStream();
function getStream() {

    // 获取canvas,若想要可视化音量,在html中增加一个高度50的,宽度500的canvas,id为meter即可,也可自行修改参数
    // canvasContext = document.getElementById("meter").getContext("2d");

    window.AudioContext = window.AudioContext || window.webkitAudioContext;


    try {
        navigator.getUserMedia =
            navigator.getUserMedia ||
            navigator.webkitGetUserMedia ||
            navigator.mozGetUserMedia;

        navigator.getUserMedia(
            {
                "audio": {
                    "mandatory": {
                        "googEchoCancellation": "false",
                        "googAutoGainControl": "false",
                        "googNoiseSuppression": "false",
                        "googHighpassFilter": "false"
                    },
                    "optional": []
                },
            }, gotStream, didntGetStream);
    } catch (e) {
        alert('getUserMedia threw exception :'   e);
    }

}


function didntGetStream() {
    alert('Stream generation failed.');
}



function gotStream(stream) {
    audioContext = new AudioContext();
    meter = createAudioMeter(audioContext);
    mediaStreamSource = audioContext.createMediaStreamSource(stream);
    
    mediaStreamSource.connect(meter);
    drawLoop();
}

function drawLoop() {
    // isMicOn是demo的判定本地流是否关闭了麦克风,如有其他状态变量判断,可以自行添加。音量大小判定边界也可自行修改
    // jquery提供的动效,以及防抖限制的时间都可自行修改。
    if (hintlock == false && meter.volume > 0.05 && isMicOn === false) {
        hintlock = true;
        $('#michint').slideDown(600);

        setTimeout(function () {
            $("#michint").slideUp(600)
            hintlock = false;
        }, 3000)
    }

    if (typeof (canvasContext) != "undefined") {
        canvasContext.clearRect(0, 0, WIDTH, HEIGHT);
        canvasContext.fillStyle = "green";
        canvasContext.fillRect(0, 0, meter.volume * WIDTH * 1.4, HEIGHT);
    }


    rafID = window.requestAnimationFrame(drawLoop);
}

function createAudioMeter(audioContext, clipLevel, averaging, clipLag) {
    var processor = audioContext.createScriptProcessor(512);
    processor.onaudioprocess = volumeAudioProcess;
    /*用法:
        audioNode = createAudioMeter (audioContext clipLevel,averaging,clipLag);你正在使用的audioContext。
        clipLevel:你会考虑“剪切”的级别(0到1)。默认为0.98。
        averaging:你希望仪表随着时间的推移变得多“平滑”。应该在0和小于1之间。默认为0.95。
        clipLag:你希望“剪辑”指示器显示多长时间,剪切发生后,以毫秒为单位。默认为750 ms。
        通过node.checkClipping()访问剪辑;使用节点。关闭以接触。
     */
    processor.clipping = false;
    processor.lastClip = 0;
    processor.volume = 0;
    processor.clipLevel = clipLevel || 0.98;
    processor.averaging = averaging || 0.95;
    processor.clipLag = clipLag || 750;

    processor.connect(audioContext.destination);

    //防抖,控制采样速度,否则音量前后会叠加。
    processor.checkClipping =
        function () {
            if (!this.clipping)
                return false;
            if ((this.lastClip   this.clipLag) < window.performance.now())
                this.clipping = false;
            return this.clipping;
        };
    
    //关闭时
    processor.shutdown =
        function () {
            this.disconnect();
            this.onaudioprocess = null;
        };

    return processor;
}

function volumeAudioProcess(event) {
    //音量计算
    var buf = event.inputBuffer.getChannelData(0);
    var bufLength = buf.length;
    var sum = 0;
    var x;


    for (var i = 0; i < bufLength; i  ) {
        x = buf[i];
        if (Math.abs(x) >= this.clipLevel) {
            this.clipping = true;
            this.lastClip = window.performance.now();
        }
        sum  = x * x;
    }

    //对样本做一个均方根:把平方加起来……
    var rms = Math.sqrt(sum / bufLength);

    //现在用对前一个样本施加的平均因子来平滑这一点——这里取最大值,因为我们想要“快速增加,缓慢下降”,这样的取值更符合显示,因为现实存在回声,音量不会下降的很快。
    this.volume = Math.max(rms, this.volume * this.averaging);
}

核心代码讲解:

  • audioContext:这是一个用于接收音频上下文的对象,是 AudioContext 的实例,这个接口在很早以前是配合audio 标签一起使用的,可以在js层面操作audio的各种功能。但后来 audio 标签逐渐废弃,但该接口依然保留,可以获取到音频实例。包括音乐文件与麦克风,不过麦克风需要做额外处理。
  • navigator.getUserMedia:熟悉 webRTC 的开发者都知道这个用于获取设备的流,chrome的话需要先赐予权限才可获取。成功执行 gotStream并获得媒体流,失败执行 didntGetStream image.pngimage.png
  • gotStream 分几步 1. 使用 audioContext 中自带的 createMediaStreamSource 方法,将从getMic中得到的上下文和getUserMedia拿到的流,再创建节点, 2. 执行 createAudioMeter,用音频上下文audioContext,创建 meter(计量表)对象 3. 将 mediaStreamSource 连接至 meter 中 audioContext; 4. 用volumeAudioProcess函数,来处理缓冲区内的音频数据即可得到音量值 }
  • createAudioMeter
代码语言:txt复制
//该段代码用于创建audio的缓冲区
function createAudioMeter(audioContext, clipLevel, averaging, clipLag) {
    var processor = audioContext.createScriptProcessor(512);
    processor.onaudioprocess = volumeAudioProcess;
    /*用法:
        audioNode = createAudioMeter (audioContext clipLevel,averaging,clipLag);你正在使用的audioContext。
        clipLevel:你会考虑“剪切”的级别(0到1)。默认为0.98。
        averaging:你希望仪表随着时间的推移变得多“平滑”。应该在0和小于1之间。默认为0.95。
        clipLag:你希望“剪辑”指示器显示多长时间,剪切发生后,以毫秒为单位。默认为750 ms。
        通过node.checkClipping()访问剪辑;使用节点。关闭以接触。
     */
    processor.clipping = false;
    processor.lastClip = 0;
    processor.volume = 0;
    processor.clipLevel = clipLevel || 0.98;
    processor.averaging = averaging || 0.95;
    processor.clipLag = clipLag || 750;

    processor.connect(audioContext.destination);

    //防抖,控制采样速度,否则音量前后会叠加。
    processor.checkClipping =
        function () {
            if (!this.clipping)
                return false;
            if ((this.lastClip   this.clipLag) < window.performance.now())
                this.clipping = false;
            return this.clipping;
        };
    
    //关闭时
    processor.shutdown =
        function () {
            this.disconnect();
            this.onaudioprocess = null;
        };

    return processor;
}
  • volumeAudioProcess
代码语言:txt复制
function volumeAudioProcess(event) {
    //音量计算
    var buf = event.inputBuffer.getChannelData(0);
    var bufLength = buf.length;
    var sum = 0;
    var x;


    for (var i = 0; i < bufLength; i  ) {
        x = buf[i];
        if (Math.abs(x) >= this.clipLevel) {
            this.clipping = true;
            this.lastClip = window.performance.now();
        }
        sum  = x * x;
    }

    //对样本做一个均方根:把平方加起来……
    var rms = Math.sqrt(sum / bufLength);

    //现在用对前一个样本施加的平均因子来平滑这一点——这里取最大值,因为我们想要“快速增加,缓慢下降”,这样的取值更符合显示,因为现实存在回声,音量不会下降的很快。
    this.volume = Math.max(rms, this.volume * this.averaging);
}

0 人点赞