乐趣区

关于前端:Web-互动连麦场景下的设备热插拔处理

事件背景是这样的~

我司有个新的 Web 端产品,同时兼容 PC 端和挪动端,次要用于线上研究、教育培训等音视频通话场景。还在测试阶段时,咱们团队外部在各种须要线上沟通的场景都会用这个产品进行连麦通话。实在的用户场景帮忙咱们发现了不少平时容易疏忽的测试用例,比方在电梯里、在车库里、在地铁上等场景通话,并一直地切换场景。明天要解决的,就是咱们产品小哥在地铁上发现的其中一个问题。

一个特定组合下的 bug:Android Chrome + 蓝牙耳机 + WebRTC

遇到问题

某一天团队内散会,到了下班时间,大家的兴致仍然很高(bushi),没有丝毫要散会的意思。然而关键时刻,产品小哥忽然收到嫂子的信息,命令他必须马上回家。会议不能忽然终止,嫂子的命令也不能不听,这不巧了,咱们的新产品在这个场景刚好能派上用场:留下来的人和产品小哥都退出线上会议房间并进行连麦通话,会议的资料通过屏幕共享和 PPT、白板等模式共享到线上会议中。这样产品小哥回家、散会都不会耽搁啦。

就这样产品小哥一边开着手机外放加入会议一边回家去了。然而到地铁上之后,产品小哥感觉外放不太公德,于是掏出蓝牙耳机,熟练地戴上耳机并胜利连贯到手机。这时意外的事件产生了,声音竟然没有从耳机播放,而是从手机扬声器输入!怎么回事?是明天耳机坏了吗?产品小哥不敢相信本人新买的耳机怎么这么快就“死于非命”了!不可能,明明明天还用这耳机听歌来着。对了,先试试还能不能听歌。(切到音乐 APP 听歌)没问题啊!(再切回来)怎么耳机还是没有声音?几番尝试之后,终于确认了出问题的不是耳机,而是咱们的新产品。

确认问题

为排除业务代码的影响,咱们通过一个简略的 demo 进行测试。这个 demo 是由 WebRTC 团队 实现的,通过根底的 WebRTC API 创立本地音视频流,并应用 video 标签进行播放。

先来理解一下 demo 中应用的 WebRTC API,后续的解决方案对这两个 API 也有肯定的依赖:

WebRTC API 作用
navigator.mediaDevices.enumerateDevices() 获取媒体输出和输出设备列表。设施类型包含音频输出设施、音频输出设备、视频输出设施。
navigator.mediaDevices.getUserMedia() 应用默认的参数或指定的设施信息等参数捕捉一个媒体流,能够应用 video 元素进行播放。

思考到 WebRTC API 对浏览器版本要求较高,并联合国人的浏览器市场份额占比进行思考,我这里只测试了 Android 下的 Chrome 浏览器,以及 iOS 下的 Safari 浏览器和 Chrome 浏览器。

测试用例及后果:

通过一番测试,根本确认遇到的问题能够在 Android Chrome 上必现。起初又在 Chromium Bug 1285166 中发现 Chromium 团队必定了该问题的存在,但单子状态被标记为“WontFix”,看来短期内只能由本人来填坑了,对 Android Chrome 进行兼容解决。

依照 Chromium Bug 1285166 的倡议,咱们本应该通过监听 devicechange 事件,而后通过 setSinkId() 切换音频输出设备。然而,在理解 setSinkId() 兼容性 后我就放弃了这个想法。

联合在测试过程中发现的一个体现——通过 getUserMedia() 应用指定音频输出设施创立视频流后,音频输入通道会主动变更为对应的设施——忽然心生一计:通过切换音频输出设施来实现切换音频输出设备的成果。再回顾一下,本来咱们的冀望成果就是要同时切换音频输出和输出设备的,这样一来岂不是两败俱伤?

解决问题

备选计划及其优缺点剖析

基于上述测试后果,临时敲定了两种解决方案:

计划一:由用户自主抉择音频输入输出设施

思路:

  1. 应用 enumerateDevices() 获取设施列表,提供一个下拉列表由用户自主抉择音频输出设施;
  2. 用户被动抉择切换设施后,应用 getUserMedia() 从新创立本地音视频流;
  3. 通过 devicechange 事件监听设施变动,并更新下拉列表。

长处:

理论应用设施与冀望设施不符的问题不确定会在什么状况下产生,所以如果无论什么时候产生问题,都能由用户自主抉择设施。

毛病:

须要由用户手动切换,老本较高。如果原本就没有音频输入(如,连麦房间内其他人都闭麦或只拉流不推流),用户无奈感知本人应用的设施与预期不符,因而可能不会手动去切换设施。

实现过程有两点须要留神:

  1. 须要应用 getUserMedia() 取得媒体设施权限后能力获取无效的设施列表。
  2. devicechange 事件兼容性较差,在 Android Chrome 上齐全不反对。

针对第 2 点,通过定时器轮询设施列表实现了兼容:

// 监听设施变更事件
function initDeviceChangeListener() {
  const isSupportDeviceChange = 'ondevicechange' in navigator.mediaDevices;
  log(`[support] 是否反对 devicechange 事件:${isSupportDeviceChange}`);
  if (isSupportDeviceChange) {navigator.mediaDevices.addEventListener('devicechange', checkDevicesUpdate);
  } else {setInterval(checkDevicesUpdate, 1000);
  }
}
​
const prevDevices = await getDevices();
async function checkDevicesUpdate() {
  // 获取变更后的设施列表,用于和 prevDevices 比对
  const devices = await getDevices();
  // 新增的设施列表
  const devicesAdded = devices.filter(device =>
    prevDevices.findIndex(({deviceId, kind}) =>device.kind === kind && device.deviceId === deviceId) < 0
  );
  // 移除的设施列表
  const devicesRemoved = prevDevices.filter(prevDevice => 
    devices.findIndex(({deviceId, kind}) => prevDevice.kind === kind && prevDevice.deviceId === deviceId) < 0
  );
  // 设施发生变化
  if (devicesAdded.length > 0 || devicesRemoved.length > 0) {// TODO}
  prevDevices = devices;
}

这个实现计划是建设在测试过程中发现的教训的根底之上:“通过 getUserMedia() 应用指定音频输出设施创立视频流后,音频输入通道会主动变更为对应的设施”。但这是否实在牢靠?

在本地开发环境中简略验证过代码可行性后,我把相干文件放到服务器上,持续应用实在的测试机进行验证(MDN 文档这里简略阐明了为什么不能应用局域网内 IP 地址来拜访测试代码)。

没想到,又踩了另一个坑:麦克风从默认设施切换到蓝牙耳机后,声音从听筒输入。呈现问题的测试机型及浏览器:Mi 11 + 微信、HUAWEI Mate 20 Pro + Chrome 94.0.4606.85。

问题起因:抉择应用非 deviceId:’default’的音频输出设施,主动切换的音频输出设备不合乎预期。详见 Chromium Bug 1277467。这里提到的“非 deviceId:’default’设施”要怎么了解呢?咱们在计划二中进行解释。

如此一来,计划一不仅不牢靠,还会引入新的问题。

计划二:通过代码为用户主动切换设施

思路:

  1. 通过 devicechange 事件监听设施变动;
  2. 有音频输出设施新增或移除时,应用零碎的 默认设施 从新创立本地音视频流。

长处:

加重用户累赘,合乎用户预期,主动切换过程无感知。

可是,零碎的默认设施从何得悉?咱们先来看看,在大部分 Android Chrome 下获取到的设施列表数据是怎么的。上面的截图是在一台红米手机的 Chrome 上实现的,截图时已连贯蓝牙耳机。

截图中能够看到,浏览器返回了 4 个设施信息(MediaDeviceInfo),设施信息中的 label 代表着该设施的形容。截图中的第一个设施示意的是零碎默认设施,前面 2、3、4 别离代表免提、耳机听筒、蓝牙耳机(即计划一中提到的“非 deviceId:’default’设施”)。其中各 Android 设施下零碎默认设施的 label 值可能存在差别,但它对应的 deviceId 都是 'default',所以咱们能够将 deviceId === 'default' 作为零碎默认设施的判断根据。

最终兼容计划

至此,问题的解决方案曾经跃然纸上了。这里先放上解决方案实现代码的 GitHub 链接。

简略概括一下步骤:

  1. 如果不是 Android,则无需解决,间接完结。
  2. 保留一份设施列表,记为 prevDevices
  3. 监听设施变更行为。
  4. 产生设施变更时,从新获取一份最新的设施列表,记为 curDevices
  5. 比照 prevDevicescurDevices,得出新增及移除的设施列表。

    1. 如果是“主播”角色:

      1. 如果新增或移除设施列表中蕴含类型为 audioinput 的设施,应用零碎默认的 audioinput 设施作为音频源从新创立一路本地音视频流。
      2. 替换本地音视频流(或仅替换本地音视频流的音轨,应用 RTCRtpSender.replaceTrack())。
    2. 如果是“观众”角色:

      1. 如果新增设施中蕴含蓝牙耳机,提醒用户可能会没有声音,能够通过刷新页面来解决。

PC 端的设施热插拔解决

为什么须要解决热插拔?

和挪动端设施热插拔相比,PC 端的浏览器实现根本没有问题,包含 devicechange 事件兼容性、切换音频输出设施后未主动切换到对应的音频输入通道等问题在 PC 上都不存在。那 PC 端须要解决的又是什么问题呢?

事件是这样的,在某次直播中,推流画面卡在了最初一帧,通过一轮排查,最初确认问题是外接摄像头的数据线接触不良。

这些状况下,咱们能做的更多是交互上的优化,比方揭示用户或提供切换设施的快捷入口等。如下为 PC 端新增设施的用户提醒效果图。

咱们的解决计划

咱们把 PC 端的设施热插拔分成了两种状况,新增设施 移除设施

新增设施时个别状况下不须要主动切换到新设施,而是揭示用户并提供切换到新设施的快捷入口。然而有一种状况例外,当新增设施是该类型设施列表中惟一的设施时,由代码外部实现主动切换。整体流程图如下:

移除设施时,只须要对移除以后在应用设施的状况做解决:主动切换到其余可用设施。整体流程如下:

两个流程中都提到的“正在应用的设施”,这个是怎么判断的呢?

很简略,先拿到正在应用的本地媒体流对象,通过 MediaStream.getVideoTracks()MediaStream.getAudioTracks() 能够别离获取到视频轨道和音频轨道对应的 MediaStreamTrack 实例;再通过 mediaStreamTrack.getSettings().deviceId 就能够得悉以后应用设施的 deviceId,从而能够判断是否为“正在应用的设施”。

实现代码:

/**
 * 从给定的设施列表找以后在应用的设施
 * @param {MediaStream} localStream - 在应用的本地音视频流
 * @param {MediaDeviceInfo[]} devices - 给定的设施列表
 * @returns MediaDeviceInfo[]
 */
function getInUseDevices(localStream, devices) {
  // localStream 为本地音视频流
  if (!localStream) return [];
  const audioTrack = localStream.getAudioTrack();
  const videoTrack = localStream.getVideoTrack();
  const inUseMic = audioTrack ? audioTrack.getSettings().deviceId : '';
  const inUseWebcam = videoTrack ? videoTrack.getSettings().deviceId : '';
  const inUseDevices = [];
  for (let i = 0; i < devices.length; i++) {const device = devices[i];
    if ((device.kind === 'audioinput' && device.deviceId === inUseMic) ||
      (device.kind === 'videoinput' && device.deviceId === inUseWebcam)) {inUseDevices.push(device);
    }
  }
  return inUseDevices;
}

其实 PC 端也有“坑”

尽管 PC 端没有 devicechange 事件的兼容性问题,然而在某些特定状况下获取设施列表的接口 navigator.mediaDevices.enumerateDevices() 也会“失灵”。目前发现的其中一种状况是,在 windows 下应用外接摄像头作为视频源时敞开页面后移除设施,从新关上页面时获取的设施列表里会返回已移除的外接设备。这种状况须要重启浏览器能力恢复正常。如果想要关注问题详情及进度能够查看 Chromium Bug 1336115。

小结

  • Android Chrome + 蓝牙耳机 + WebRTC 存在音频输入通道未切换到蓝牙耳机的问题,须要自行进行设施兼容解决。
  • PC 端不存在兼容问题,更多的是要从交互上做优化解决,须要别离思考设施新增和移除这两种状况。
  • 能够在 这里 查看 Chromium 的已知问题。相应地,WebKit 也有其对应的 bug 列表。
退出移动版