WebRTC 之媒体设备列表 enumerateDevices

2,429 阅读4分钟

本文正在参加「金石计划 . 瓜分6万现金大奖」

主要介绍了 enumerateDevices() 获取媒体设备列表的通用用法,包含麦克风、摄像头的设备分类;摄像头设备前后置的区分和处理;摄像头前后置的切换处理;避免广角摄像头的选择等等

获取媒体设备列表

通过 navigator.mediaDevices.enumerateDevices 来获取媒体设备列表数据,包括摄像头和麦克风。

注意:此处一个接口中的数据类型有两种,分别为 InputDeviceInfoMediaDeviceInfo

// 获取设备列表
function getDevices() {
  return new Promise((resolve, reject) => {
    navigator.mediaDevices
      .enumerateDevices()
      .then((devices) => {
        console.log("设备列表数据,整理前", devices);

        // 临时展示模块 待todo替换
        let mediaList = [];
        devices.forEach((device) => {
          mediaList.push({
            id: device.deviceId,
            label: device.label,
            kind: device.kind,
          });
        });
        console.log("设备列表数据,整理后", mediaList);
        resolve({ flag: true, devices: mediaList });
      })
      .catch((err) => {
        console.error(err.name + ": " + err.message);
        reject({ flag: false, err });
      });
  });
}

enumerateDevices_1.png

媒体设备分类

实际应用中,麦克风、摄像头列表可以分开展示,因为麦克风和摄像头是可以同时存在的

如上图所示,可以通过kind属性来进行区分:视频设备类型为 videoinput;音频设备类型为 audioinputaudiooutput(外放设备)。

音频设备,想区分的更细致,可以用两个列表来展示,但是一般没有必要。

getDevices()方法中的“临时展示模块”,替换为如下方式:

// todo
let audioDevices = [];
let videoDevices = [];
devices.forEach((device) => {
  // 没有授予硬件权限时,deviceId为空字符串
  if (device.deviceId == "communications" || device.deviceId == "") {
    return;
  }
  if (device.kind == "videoinput") {
    // 视频设备
    videoDevices.push(device);
  } else {
    // 音频设备
    audioDevices.push(device);
  }
});

if (videoDevices.length === 0) {
  reject({ flag: false, err: "未检测到摄像头" });
  return;
}
if (audioDevices.length === 0) {
  reject({ flag: false, err: "未检测到麦克风" });
  return;
}
// resolve({ flag: true, videoDevices, audioDevices });

前后置摄像头区分

对于 PC 端,只有前置摄像头,一般情况下,只有一个摄像头设备;当然也可以外接摄像头设备,到时候就可以选择打开的摄像头设备了

对于手机端,前后置摄像头是都有的。一般手机通过enumerateDevices()方法获取的前后置摄像头各只有一个;但是有些手机,如华为手机,或者某些手机的火狐浏览器,获取到的设备列表有多个。

区分前后置摄像头:

  1. 一般可以通过 label 属性区分,但是也有可能 label 值为空,就需要进行单独的处理了
  2. 同一个华为手机上,自带的浏览器可获取到 7 个摄像头,但是 QQ 浏览器只有 2 个(前后置)摄像头,浏览器底层解析返回的也是不一样的
  3. 对于华为这种多摄像头的情况,要注意选择正确的摄像头,避免选中广角的摄像头,导致看到的画面不清晰

enumerateDevices_2.png

// todo 如下代码接着上面分类继续
// 后置摄像头列表
let cameraBackList = videoDevices.filter(
  (o) => o.label.indexOf("back") > -1 || o.label.indexOf("后") > -1
);
// 前置摄像头列表
let cameraPreList = videoDevices.filter(
  (o) => !(o.label.indexOf("back") > -1 || o.label.indexOf("后") > -1)
);
// 没有后置时,取前置最后一个
if (cameraBackList.length === 0) {
  cameraBackList.push(cameraPreList[cameraPreList.length - 1]);
}
if (cameraPreList.length === videoDevices.length) {
  cameraPreList.length = 1;
}
console.log("前置摄像头列表", cameraPreList);
console.log("后置摄像头列表", cameraBackList);
// 前置正常取第二个;只有一个时,取第一个
let preCameraId = cameraPreList[cameraPreList.length > 1 ? 1 : 0].deviceId;
// 后置正常取最后一个;火狐后置取第一个;华为后置也是取第一个(解决华为火狐浏览器后置选中广角摄像头的问题)
let ua = navigator.userAgent;
let backCameraId =
  cameraBackList[
    ua.indexOf("Firefox") || ua.indexOf("HUAWEI")
      ? 0
      : cameraBackList.length - 1
  ].deviceId;
// 麦克风默认取第一个 safari不支持扬声器,即没有audiooutput
let microphoneId = audioDevices[0].deviceId;
console.log("前置摄像头id", preCameraId);
console.log("后置摄像头id", backCameraId);
console.log("麦克风id", microphoneId);
resolve({ flag: true, preCameraId, backCameraId });

enumerateDevices_3.png

摄像头的切换

上面的通用方法:

  • 对于 web 端,就是进行摄像头的切换
  • 对于手机端,可以进行前后置摄像头的切换
  1. 获取前后置摄像头 ID ,方便后面进行切换操作
// 需要用到的参数
let localStream = null;
let preCameraId = null;
let backCameraId = null;
let isPre = true;

// 获取前后置摄像头ID
const res = await getDevices();
preCameraId = res.preCameraId;
backCameraId = res.backCameraId;
  1. 切换视频流的操作,具体方法可看上一篇文章WebRTC 之媒体权限申请 getUserMedia

传参时,可以通过设备ID(deviceId)指定打开某个摄像头,具体实现deviceId: isPre ? backCameraId : preCameraId

// 切换前需要先关闭流
if (localStream) {
  localStream.getTracks().forEach((stream: MediaStreamTrack) => {
    stream.stop();
  });
}
// 预览视频流方法
getUserMedia(
  {
    video: { deviceId: isPre ? backCameraId : preCameraId },
    audio: true,
  },
  (stream) => {
    localStream = stream;
    localVideo.current.srcObject = stream;
    isPre = !isPre;
  }
);