TRTC Web端 仿腾讯会议麦克风静音检测
项目背景
目前 Web 的 TRTC 没有静音检测,在关闭麦克风的情况下发言没有提示,有时候会有比较尴尬的会议场景出现,为提升用户体验,这里尝试将腾讯会议的解决思路引入。
当用户在关闭麦克风的情况下,如果周围声音超过一定的分贝值,则显示提示。
目前可兼容主流PC浏览器下的所有框架,electron框架,为更好接入TRTC Web Demo 采用jquery库开发(主要是交互与样式),稍微改动即可适配原生与其他框架。
这份代码直接在TRTC的Demo中写一份js文件引入即可,即插即用。如果有需要自定义的部分,我也在代码里写了注释。
使用方法:
注:该功能需要在服务端才可使用,调试请使用Live Server。
效果展示:
源代码
说起来很简单,实际做起来有些难度,根本上是需要用js去加工音频流才行,用到了一个很旧的接口audioContext以及createMediaStreamSource相关的方法才得以实现,这里直接贴结果代码。
- 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.png - gotStream 分几步
1. 使用 audioContext 中自带的 createMediaStreamSource 方法,将从getMic中得到的上下文和getUserMedia拿到的流,再创建节点,
2. 执行 createAudioMeter,用音频上下文audioContext,创建 meter(计量表)对象
3. 将 mediaStreamSource 连接至 meter 中
audioContext;
4. 用volumeAudioProcess函数,来处理缓冲区内的音频数据即可得到音量值
} - createAudioMeter
- //该段代码用于创建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
- 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);
- }