这篇文章上次修改于 524 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

公司的项目使用了声网 SDK 实现语音通话,但在其他同事接入了另外一个功能后,被发现断开麦克风结束通话后,浏览器窗口上显示“小红点”,依旧占用麦克风的情况。这很容易导致客户认为我们在继续“监听”他说话,影响使用体验。这是一个遗留很久的 Bug,一直都没有人找出具体的原因,但在今天,我终于有了新的发现!查看日记全文

小红点

我打算使用浏览器的原生方法,实现一个获取麦克风源并使用播放器实时播放的功能,简单模拟使用麦克风并消除占用的过程。如果能在这个最小的代码示例里成功复现一样的“小红点”占用效果,就需要好好检查下对应的库是否存在产生此问题的代码了。

使用 navigator.mediaDevices.getUserMedia() 获取源,再使用 getAudioTracks() 方法成功获取到麦克风的轨道。按照公司项目的源代码,应该要把这个轨道复制到另外一个源里。

const stream = await navigator.mediaDevices.getUserMedia(constraints);
let newStream = new MediaStream();

const audioTracks = stream.getAudioTracks();

audioTracks.forEach(item => {
  newStream.addTrack(item);
});

想要浏览器对应的窗口停止占用麦克风,则需要停止该轨道对麦克风声音的实时获取。使用 MediaStreamTrack 的 stop() 方法就可以实现,执行后我浏览器上的“小红点”确实消失了。

const newStreamTracks = newStream.getTracks();

newStreamTracks.forEach(item => {
  // 只要 stop 理论上就会停止占用麦克风设备了
  item.stop();
});

期间通过使用 MDN 查询文档,发现 MediaStreamTrack 有一个叫做 clone方法,该方法将返回复制的轨道对象,主要可见变化是更换了其 id

我尝试将它应用到 我的代码 里面,之后打算停止占用,结果“小红点”确实出现了无法消失,始终占用的情况。

audioTracks.forEach(item => {
  // newStream.addTrack(item);
  // ! 只要使用 clone 方法,就会导致 stop 轨道时,浏览器依旧显示小红点(麦克风设备使用中)
  newStream.addTrack(item.clone());
});
经过 Google 后发现,貌似存在类似的 问题,属于浏览器内核的 Bug。但保罗还是比较菜,就没有就此问题继续研究了。

这就已经有了不小的发现了,接下来需要排查下是否是某个第三方库执行了类似的代码所致。我们项目除了声网以外,还使用到了另外一个服务,也需要引用当前用户使用的麦克风源。

将声网 SDK 返回的麦克风源替换为其内置的方法(会返回第一个麦克风源,假设你有多个麦克风,则会触发另外一个麦克风音源不对的 Bug)后,“小红点”并没有始终显示的情况。所以基本上可以确认是声网 SDK 返回的轨道可能是 clone 过的,导致“小红点”始终显示,该 SDK 的源码闭源,但我也从压缩混淆的代码里找到了类似的 clone 方法。

clone(t, r, i, n) {
  const o = this._mediaStreamTrack.clone();
  return new e(o, t, r, i, n)
}

上面这么多的差分测试我觉得已经足够说明问题,SDK 代码内部的具体情况,我这边就不细探了。

知道了问题产生的原因,却没有好的解决办法。接下来可能会就此问题向声网那边提工单,看看他们那边的回复了。有一说一,第一次解决这种疑难杂症,还是挺有成就感的!

以下内容于 9.27 日补充:

给同事看了本文提供的最小 Demo,他那边修改了下,其实只需要将 clone() 前后的所有 MediaStreamTrack 都 stop() 掉即可完成释放了。也就是说 clone() 之后就完全新增了一个占用项,只要有一个没有释放,就会存在“小红点”的占用效果。

const newStreamTracks = newStream.getTracks();

// 原流所有轨道
audioTracks.forEach(item => {
  item.stop();
});
// 新流的所有轨道
newStreamTracks.forEach(item => {
  // 只要 stop 理论上就会停止占用麦克风设备了
  item.stop();
});

那问题就变成了两种可能性,一个是 React Hooks 编写逻辑问题,导致反复获取到新的 clone() 后的轨道。再一个就是“那另外一个服务”是否是将传入的轨道持续占用,而没有提供对应的销毁代码?这个问题还需要写一个最小复现,从而排除下是第三方的问题还是 Hooks 逻辑的问题。