JS调用摄像头及常见错误处理

因业务需求,做了一段事件webRTC直播相关项目的开发。为避免遗忘,整理一下JS获取设备信息及常见错误处理方法,以便后期查阅搬砖。

检测mediaDevices调用是否受限

由于受浏览器的限制,navigator.mediaDevices.getUserMedia在https协议下是可以正常使用的,而在http协议下只允许localhost/127.0.0.1这两个域名访问,因此在开发时应做好容灾处理,上线时则需要确认生产环境是否处于https协议下。

1
2
3
4
5
let mediaDevices = navigator.mediaDevices || null 
if ( mediaDevices === null ) {
console.warn(`请确定是否处于https协议环境下`)
return
}

获取权限的通用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
getUserMedia(constraints = {
video: true,
audio: true
}) {
return new Promise((resolve, reject) => {
navigator.mediaDevices.getUserMedia(constraints).then(stream => {
resolve(stream)
}).catch(err => {
//log to console first
console.log(err); /* handle the error */
let toastText = '未知错误'
let errName = err.name
if (errName == "NotFoundError" || errName == "DevicesNotFoundError") {
//required track is missing
toastText = '未发现设备'
} else if (errName == "NotReadableError" || errName == "TrackStartError") {
//webcam or mic are already in use
toastText = '设备被占用'
} else if (errName == "OverconstrainedError" || errName == "ConstraintNotSatisfiedError") {
//constraints can not be satisfied by avb. devices
toastText = '约束无法满足(分辨率)'
} else if (errName == "NotAllowedError" || errName == "PermissionDeniedError") {
//permission denied in browser
toastText = '请求被拒绝'
} else if (errName == "TypeError" || errName == "TypeError") {
//empty constraints object
toastText = '必须至少请求一个音频和视频'
} else {
//other errors
}
reject({
toastText,
errName
})
});
})
}

获取设备(摄像头、麦克风、扬声器)列表

我在项目开发中需要用到的硬件参数主要有两种:品牌,分辨率。获取摄像头的品牌名称相对来说比较简单,可直接通过mediaDevices.enumerateDevices()获取电脑上可使用的外设列表,通过kind字段过滤出摄像头。
提示:必须先获取权限才能拿到设备列表

1
2
3
4
if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
console.error(location.href + ":不支持 enumerateDevices() .")
return
}

完整获取设备列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 获取系统设备
async getDevices() {
if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
console.error(location.href + ":不支持 enumerateDevices() .")
return
}
// 初始化设备
const devices = await navigator.mediaDevices.enumerateDevices()
console.log(devices)
var camList = [],micList = [],spkList = [];
for (var i = 0; i !== devices.length; ++i) {
var device = devices[i];
var option = {}
option.value = device.deviceId;
if (device.kind === 'audioinput') {
option.text = device.label || 'microphone ' + (micList.length + 1);
micList.push(option);
} else if (device.kind === 'videoinput') {
option.text = device.label || 'camera ' + (camList.length + 1);
camList.push(option);
} else if (device.kind === 'audiooutput') {
option.text = device.label || 'speaker ' + (spkList.length + 1);
spkList.push(option);
}else{
console.log('Some other kind of source/device: ', device);
}
}
return {
camList,
micList,
spkList,
camSelected: this.getDefaultDevice(camList),
micSelected: this.getDefaultDevice(micList),
spkSelected: this.getDefaultDevice(spkList),
}
}

设备变更

在摄像头拔出的一瞬间,active会从true变更为false,同时触发oninactive钩子,有了状态监听之后事情就简单了许多。代码经过测试后发现,对用户变更摄像头权限也有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 判断摄像头是否在线
let cameraIsOnline = false

const loadWebCamera = () => {
...
mediaDevices.getUserMedia({ audio: false, video: true }).then(async (stream) => {
const video = document.getElementById('videoPlayer');
if (window.URL) {
try {
video.src = window.URL.createObjectURL(stream);
} catch (err) {
video.srcObject = stream;
}
} else {
video.src = stream;
}
video.autoplay = true;
// 兼容性处理
if( stream.oninactive === null ) {
// 监听流中断,流中断后将重新进行调用自身进行状态监测
stream.oninactive = () => loadWebCamera()
}
await video.play()
cameraIsOnline = true
}).catch((error) => {
cameraIsOnline = false
...
})
}

JS调用摄像头及常见错误处理
http://example.com/20210329-JS调用摄像头及常见错误处理/
作者
csorz
发布于
2021年3月29日
许可协议