5.5 设备枚举
用户的音视频输入/输出设备可能有多个,使用时需要确定选择哪个设备,所以需要设备选择功能供用户挑选合适的设备。这里的输入设备指麦克风、摄像头、视频采集卡等硬件,输出设备指内置扬声器、外围扬声器、头戴式耳机等硬件。
MediaDevices的方法enumerateDevices()可请求一个输入和输出设备的列表,例如麦克风、摄像机、耳机设备等。语法如下面的代码所示。
var enumeratorPromise = navigator.mediaDevices.enumerateDevices();
上面的代码会返回一个Promise。当完成时,Promise接收一个MediaDeviceInfo对象的数组,每个对象描述一个可用的媒体输入/输出设备。如果枚举失败,Promise也会被拒绝(rejected)。MediaDeviceInfo包含了设备的几个重要信息,如下所示。
·deviceId:设备Id,用一个唯一的字符串来区分不同的硬件设备。
·kind:设备类型,分别为视频输入设备(videoinput)、音频输入设备(audioinput)、音频输出设备(audiooutput)。
·label:设备名称,如FaceTimeHD。
设置类型、设备名称、设备Id的示例如下所示,可以看到,此计算机上为一个苹果的摄像头及一个内置的麦克风。
设备类型 videoinput 设备名称 FaceTime HD Camera (Built-in) 设备Id csO9c0YpAf274OuCPUA53CNE0YHlIr2yXCi+SqfBZZ8= 设备类型 audioinput 设备名称 default (Built-in Microphone) 设备Id RKxXByjnabbADGQNNZqLVLdmXlS0YkETYCIbg+XxnvM=
由于用户的设备不是每个都能使用,当测试好某个设备功能后最好添加一个本地保存功能,这样下次再使用时就会记住此设备。HTML5里可以使用localStorage来进行存取。
当获取到用户的输入设备Id后,需要把值设置到getUserMedia()的约束条件里,如下面的代码所示。
let constraints = { //设置音频设备Id audio: { deviceId: audioSource ? { exact: audioSource } : undefined }, //设置视频设备Id video: { deviceId: videoSource ? { exact: videoSource } : undefined } };
上面代码的deviceId即为使用设备枚举方法返回的设备Id,如果不设置这个Id值则会调用默认设备作为输入源。
知道如何选择音视频的输入设备了,那么音视频的输出设备如何选择呢?这里首先要了解一下HTMLMediaElement这个接口。它是HTML5里video和audio的基类,即媒体对象,可以通过它的setSinkId方法来改变输出源,要传的参数即为音频输出设备的Id。语法如下所示。
HTMLMediaElement.setSinkId(sinkId).then(function() { ... })
接下来,通过一个视频滤镜的示例来展示滤镜的效果。具体步骤如下所示。
步骤1 打开h5-samples工程下的src目录,添加DeviceSelect.jsx文件。定义以下几个状态变量,用于存储当前不同类型的设备列表以及记录当前选择的设备Id。
·当前选择的音频输入设备。
·当前选择的音频输出设备。
·当前选择的视频输入设备。
·视频输入设备列表。
·音频输入设备列表。
·音频输出设备列表。
步骤2 编写更新设备列表的方法,使用navigator.mediaDevices.enumerateDevices接口获取所有设备,然后根据设备类型分别放入三个设备列表,之后返回数据。大致处理如下所示。
//更新设备列表 updateDevices = () => { return new Promise((pResolve, pReject) => { //设备列表 ... //枚举所有设备 navigator.mediaDevices.enumerateDevices() //返回设备列表 .then((devices) => { //使用循环迭代设备列表 for (let device of devices) { //将不同的设备存储至设备列表里 ... } }).then(() => { //处理好后将三种设备数据返回 ... }); }); }
步骤3 根据获取到的音视频输入设备Id设置约束条件,然后调用getUserMedia()方法获取媒体流。大致处理如下所示。
startTest = () => { //设备Id ... //定义约束条件 let constraints = { //设置音频设备Id audio: { deviceId: audioSource ? { exact: audioSource } : undefined }, //设置视频设备Id video: { deviceId: videoSource ? { exact: videoSource } : undefined } }; //根据约束条件获取数据流 navigator.mediaDevices.getUserMedia(constraints) .then((stream) => { //成功返回音视频流 ... }) ... }
步骤4 编写音频输入、视频输入以及音频输出设备改变回调方法。其中,音频输出需要调用setSinkId()方法来改变输出源,处理的关键代码如下所示。
handleAudioOutputDeviceChange = (e) => { ... //调用HTMLMediaElement的setSinkId()方法改变输出源 videoElement.setSinkId(e) ... }
步骤5 在页面渲染部分添加设备下拉列表框。使用数组的map()方法迭代出所有设备信息。以音频输入设备列表为例,处理代码如下所示。
this.state.audioDevices.map((device, index) => { return ( <Option value={device.deviceId} key={device.deviceId}>{device.label} </Option> ); })
上面的代码使用了device.label作为列表项的显示值,即设备名称。
然后在src目录下的App.jsx及Samples.jsx里加上链接及路由绑定,这可参考第3章。完整的代码如下所示。
import React from "react"; import { Button, Select } from "antd"; const { Option } = Select; //视频对象 let videoElement; /** * 输入输出设备选择示例 */ class DeviceSelect extends React.Component { constructor() { super(); this.state = { //当前选择的音频输入设备 selectedAudioDevice: "", //当前选择的音频输出设备 selectedAudioOutputDevice: "", //当前选择的视频输入设备 selectedVideoDevice: "", //视频输入设备列表 videoDevices: [], //音频输入设备列表 audioDevices: [], //音频输出设备列表 audioOutputDevices: [], } } componentDidMount() { //获取视频对象 videoElement = this.refs['previewVideo']; //更新设备列表 this.updateDevices().then((data) => { //判断当前选择的音频输入设备是否为空并且是否有设备 if (this.state.selectedAudioDevice === "" && data.audioDevices.length > 0) { this.setState({ //默认选中第一个设备 selectedAudioDevice: data.audioDevices[0].deviceId, }); } //判断当前选择的音频输出设备是否为空并且是否有设备 if (this.state.selectedAudioOutputDevice === "" && data.audioOutputDevices. length > 0) { this.setState({ //默认选中第一个设备 selectedAudioOutputDevice: data.audioOutputDevices[0].deviceId, }); } //判断当前选择的视频输入设备是否为空并且是否有设备 if (this.state.selectedVideoDevice === "" && data.videoDevices.length > 0) { this.setState({ //默认选中第一个设备 selectedVideoDevice: data.videoDevices[0].deviceId, }); } //设置当前设备Id this.setState({ videoDevices: data.videoDevices, audioDevices: data.audioDevices, audioOutputDevices: data.audioOutputDevices, }); }); } //更新设备列表 updateDevices = () => { return new Promise((pResolve, pReject) => { //视频输入设备列表 let videoDevices = []; //音频输入设备列表 let audioDevices = []; //音频输出设备列表 let audioOutputDevices = []; //枚举所有设备 navigator.mediaDevices.enumerateDevices() //返回设备列表 .then((devices) => { //使用循环迭代设备列表 for (let device of devices) { //过滤出视频输入设备 if (device.kind === 'videoinput') { videoDevices.push(device); //过滤出音频输入设备 } else if (device.kind === 'audioinput') { audioDevices.push(device); //过滤出音频输出设备 } else if (device.kind === 'audiooutput') { audioOutputDevices.push(device); } } }).then(() => { //处理好后将三种设备数据返回 let data = { videoDevices, audioDevices, audioOutputDevices }; pResolve(data); }); }); } //开始测试 startTest = () => { //获取音频输入设备Id let audioSource = this.state.selectedAudioDevice; //获取视频输入设备Id let videoSource = this.state.selectedVideoDevice; //定义约束条件 let constraints = { //设置音频设备Id audio: { deviceId: audioSource ? { exact: audioSource } : undefined }, //设置视频设备Id video: { deviceId: videoSource ? { exact: videoSource } : undefined } }; //根据约束条件获取数据流 navigator.mediaDevices.getUserMedia(constraints) .then((stream) => { //成功返回音视频流 window.stream = stream; videoElement.srcObject = stream; }).catch((err) => { console.log(err); }); } //音频输入设备改变 handleAudioDeviceChange = (e) => { console.log('选择的音频输入设备为: ' + JSON.stringify(e)); this.setState({ selectedAudioDevice: e }); setTimeout(this.startTest, 100); } //视频输入设备改变 handleVideoDeviceChange = (e) => { console.log('选择的视频输入设备为: ' + JSON.stringify(e)); this.setState({ selectedVideoDevice: e }); setTimeout(this.startTest, 100); } //音频输出设备改变 handleAudioOutputDeviceChange = (e) => { console.log('选择的音频输出设备为: ' + JSON.stringify(e)); this.setState({ selectedAudioOutputDevice: e }); if (typeof videoElement.sinkId !== 'undefined') { //调用HTMLMediaElement的setSinkId()方法改变输出源 videoElement.setSinkId(e) .then(() => { console.log(`音频输出设备设置成功: ${sinkId}`); }) .catch(error => { if (error.name === 'SecurityError') { console.log(`你需要使用HTTPS来选择输出设备: ${error}`); } }); } else { console.warn('你的浏览器不支持输出设备选择。'); } } render() { return ( <div className="container"> <h1> <span>输入输出设备选择示例</span> </h1> {/* 音频输入设备列表 */} <Select value={this.state.selectedAudioDevice} style={{ width: 150,marginRight:'10px' }} onChange={this.handleAudioDeviceChange}> { this.state.audioDevices.map((device, index) => { return (<Option value={device.deviceId} key={device. deviceId}>{device.label}</Option>); }) } </Select> {/* 音频输出设备列表 */} <Select value={this.state.selectedAudioOutputDevice} style={{ width: 150,marginRight:'10px' }}onChange={this.handleAudioOutputDeviceChange}> { this.state.audioOutputDevices.map((device, index) => { return (<Option value={device.deviceId} key={device.deviceId}>{device.label}</Option>); }) } </Select> {/* 视频频输入设备列表 */} <Select value={this.state.selectedVideoDevice} style={{ width: 150 }} onChange={this.handleVideoDeviceChange}> { this.state.videoDevices.map((device, index) => { return (<Option value={device.deviceId} key={device.deviceId}>{device.label}</Option>); }) } </Select> {/* 视频预览展示 */} <video className="video" ref='previewVideo' autoPlay playsInline style={{ objectFit: 'contain',marginTop:'10px' }}></video> <Button onClick={this.startTest}>测试</Button> </div> ); } } //导出组件 export default DeviceSelect;
运行示例程序后,可以选择不同的麦克风、摄像头以及扬声器,然后点击“测试”按钮,运行效果如图5-3所示。图中第一个下拉列表框中为麦克风设备列表,第二个下拉列表框中为扬声器设备列表,第三个下拉列表框中为摄像头列表。
图5-3 设备枚举示例效果图
有的计算机上显示有多个设备,但实际设备可能只有一个麦克风和一个扬声器。出现这种情况的主要原因是曾经安装了驱动而没有卸载,当选择了这种设备后,通常无法正常使用。