WebRTC音视频开发:React+Flutter+Go实战
上QQ阅读APP看书,第一时间看更新

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 设备枚举示例效果图

有的计算机上显示有多个设备,但实际设备可能只有一个麦克风和一个扬声器。出现这种情况的主要原因是曾经安装了驱动而没有卸载,当选择了这种设备后,通常无法正常使用。