获取摄像头/麦克风权限

5,046 阅读2分钟

使用 WebRTC 时,调用 mediaDevices 相关接口必须要先使用 getUserMedia 获取摄像头/麦克风权限

版本一:直接获取权限

// 释放 mediaStream
const stopStreamTracks = stream => {
  if (!stream || !stream.getTracks) {
    return;
  }
  try {
    const tracks = stream.getTracks();
    tracks.forEach(it => {
      try {
        it.stop();
      } catch (errMsg) {
        // debugger;
      }
    });
  } catch (errMsg) {
    // debugger;
  }
};
// 请求 摄像头/麦克风 权限
const requestPermission = async () => {
  try {
    const stream = await global.navigator.mediaDevices.getUserMedia({ video: true, audio: true });
    stopStreamTracks(stream);
    if (stream) {
      return true;
    }
  } catch (errMsg) {
    if (errMsg && 'NotAllowedError' === errMsg.name) {
      return false;
    }
  }
  return true;
};

正常情况下可以工作,但是当电脑没有摄像头或者麦克风是,并不会弹出权限允许的弹窗,此时 getUserMedia 直接 rejected,返回了 device not found 异常,基于这样的问题,开发了版本二。

版本二:过滤设备列表,动态请求设备权限

// 获取设备列表
const enumerateDevices = async () => {
  try {
    const devices = await global.navigator.mediaDevices.enumerateDevices();
    return devices;
  } catch (errMsg) {
    return [];
  }
};

// 请求 摄像头/麦克风 权限
const requestPermission = async () => {
  const devices = await enumerateDevices();
  const constraints = devices.reduce(
    (info, device) => {
      if (!device) {
        return info;
      }
      if ('videoinput' === device.kind) {
        return { video: true, audio: info.audio };
      }
      if ('audioinput' === device.kind) {
        return { audio: true, video: info.video };
      }
      return info;
    },
    { video: false, audio: false },
  );
  try {
    const stream = await global.navigator.mediaDevices.getUserMedia(constraints);
    stopStreamTracks(stream);
  } catch (errMsg) {
    if (errMsg && 'NotAllowedError' === errMsg.name) {
      return false;
    }
  }
  return true;
};

此版本也正常工作了一段时间,直到碰到了在 Linux 下使用 PCIE 采集卡的客户,因为在 getUserMedia 之前调用了 enumerateDevices,在 linux 下的 chrome 中使用 4 路 PCIE 采集卡,设备列表返回的设备 deviceId 位空且只有一个设备,与预期不符,基于这样的问题,开发了版本三。

版本三:逐步回退,多次检查

  • 第一步:尝试同时获取摄像头和麦克风权限,如果是 NotAllowedError 异常的话说明用户拒绝提供权限,否则进入下一步
  • 第二步:单独获取麦克风权限(业务选择麦克风比摄像头重要),如果异常检查原因,进入下一步
  • 第三步:单独获取摄像头权限,检查异常原因
  • 第四步:如果三次异常都不是 NotAllowedError导致的,那么可以认为是设备异常导致的,不要提示用户权限不够
// 请求 摄像头/麦克风 权限
const requestPermission = async () => {
  // 第一步:尝试同时获取摄像头和麦克风权限
  try {
    const stream = await global.navigator.mediaDevices.getUserMedia({ video: true, audio: true });
    stopStreamTracks(stream);
    if (stream) {
      return true;
    }
  } catch (errMsg) {
    if (errMsg && 'NotAllowedError' === errMsg.name) {
      return false;
    }
  }
  try {
    const stream = await global.navigator.mediaDevices.getUserMedia({ video: false, audio: true });
    stopStreamTracks(stream);
    if (stream) {
      return true;
    }
  } catch (errMsg) {
    if (errMsg && 'NotAllowedError' === errMsg.name) {
      return false;
    }
  }
  try {
    const stream = await global.navigator.mediaDevices.getUserMedia({ video: true, audio: false });
    stopStreamTracks(stream);
    if (stream) {
      return true;
    }
  } catch (errMsg) {
    if (errMsg && 'NotAllowedError' === errMsg.name) {
      return false;
    }
  }
  return true;
};

版本三暂时没有发现问题,后续有问题继续更新。

稍微优化了一下代码

function getDevicePermission(constraints) {
  return global.navigator.mediaDevices
    .getUserMedia(constraints)
    .then(stream => {
      if (stream) {
        stopStreamTracks(stream);
        return true;
      }
      return Promise.reject(new Error('EmptyStreamError'));
    })
    .catch(errMsg => {
      if (errMsg && 'NotAllowedError' === errMsg.name) {
        return false;
      }
      return Promise.reject(errMsg);
    });
}

function requestPermission() {
  return getDevicePermission({ video: true, audio: true })
    .catch(() => getDevicePermission({ video: false, audio: true }))
    .catch(() => getDevicePermission({ video: true, audio: false }))
    .catch(() => true);
}