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

5.6 设置综合示例

在视频会议、视频会诊以及远程教育系统里,需要加一个媒体设置功能,通常需要包含以下几块内容。

·选择麦克风

·麦克风音量展示

·选择摄像头

·摄像头视频预览

·选择清晰度(分辨率)

·选择码率(带宽)

对于这些设置,用户可以在进入房间(如会议房间)前设置好,也可以在房间里动态设置。其中,分辨率越高,画质越清晰。码率决定了视频传输的速度,码率越高,所需要的带宽越大。所选择的麦克风如果不显示音量条,则说明此麦克风设备无法使用。所选择的摄像头如果不呈现视频预览,则说明此摄像头设备无法使用。

接下来,我们通过一个综合示例把前面阐述的知识串起来,注意此示例里不包含带宽的设置。具体步骤如下。

步骤1 打开h5-samples工程下的src目录,添加media-settings文件夹。添加媒体设置文件MediaSettings.jsx以及音量处理文件soundmeter.js。另外,在style/css目录下添加样式文件media-settings.scss。其中项目文件主要用于弹出对话框项布局,这里不过多描述。

步骤2 添加分辨率设置、音量检测以及设备枚举等处理代码,这些处理参照5.2~5.4节即可。

步骤3 添加本地存储功能,当用户选择好设备并点击“确定”按钮后,需要将当前设置的信息保存起来。处理代码如下所示。


let deviceInfo = {
    //音频设备Id
    ...
    //视频设备Id
    ...
    //分辨率
    ...
};
//使用JSON转成字符串后存储在本地
localStorage["deviceInfo"] = JSON.stringify(deviceInfo);

存储信息时需要将Map转成String类型,可以使用JSON.stringify方法。当用户下次再打开设备时,需要将之前保存的信息再提取出来,代码如下所示。


let info = JSON.parse(deviceInfo);

提取信息时需要将String转成Map类型,可以使用JSON.parse方法实现。

步骤4 示例中需要添加关闭本地媒体流方法,需要同时关闭音频流及视频流。大致处理代码如下所示。


closeMediaStream = (stream) => {
    ...
    //判断是否有getTracks方法
    if (stream.getTracks) {
        //获取所有Track
        ...
        //迭代所有Track
        for (...) {
            //停止每个Track
            ...
        }
    } else {
        //获取所有音频Track
        tracks = stream.getAudioTracks();
        //迭代所有音频Track
        for (...) {
            //停止每个Track
            ...
        }
        //获取所有视频Track
        tracks = stream.getVideoTracks();
        for (...) {
            //停止每个Track
         }
    }
}

上述代码首先获取stream里的轨道,然后使用循环语句逐个停掉。这里采用两个判断,是针对浏览器API差异做的不同处理。

当不使用某个设备后,停止流是有必要的,目的是防止出现设备占用的情况。图5-4中的提示表示有设备正在使用Chrome浏览器。

图5-4 设备占用示意图

在测试音视频项目时,如果遇到设备打不开、音视频不能正常播放的情况,可以首先查看一下设备是否被某个软件占用。设备启用时笔记本电脑上会有一个绿色的小灯亮起,同时Chrome浏览器中会显示红色圆圈。

步骤5 在页面渲染部分添加设备下拉列表框、分辨率下拉列表框、音量展示、视频预览等标签。使用数组的map方法迭代出所有的设备信息。这些处理参照5.2~5.4节。完整代码如下所示。


import React from 'react';
import { Modal, Button, Select } from 'antd';
import SoundMeter from './soundmeter';
import '../../styles/css/media-settings.scss';

const Option = Select.Option;
/**
 * 音频、视频、分辨率、综合设置
 */
export default class MediaSettings extends React.Component {

    constructor(props) {
        super(props)
        this.state = {
            //是否弹出对话框
            visible: false,
            //视频输入设备列表
            videoDevices: [],
            //音频输入设备列表
            audioDevices: [],
            //音频输出设备列表
            audioOutputDevices: [],
            //分辨率
            resolution: 'vga',
            //当前选择的音频输入设备
            selectedAudioDevice: "",
            //当前选择的视频输入设备
            selectedVideoDevice: "",
            //音频音量
            audioLevel: 0,
        }

        try {
            //AudioContext是用于管理和播放所有的声音
            window.AudioContext = window.AudioContext || window.webkitAudioContext;
            //实例化AudioContext
            window.audioContext = new AudioContext();
        } catch (e) {
             console.log('网页音频API不支持。');
        }
    }

    componentDidMount() {
        if (window.localStorage) {
            //读取本地存储的信息
            let deviceInfo = localStorage["deviceInfo"];
            if (deviceInfo) {
                //将JSON数据转成对象
                let info = JSON.parse(deviceInfo);
                //设置本地状态值
                this.setState({
                    selectedAudioDevice: info.audioDevice,
                    selectedVideoDevice: info.videoDevice,
                    resolution: info.resolution,
                });
            }
        }
        //更新设备
        this.updateDevices().then((data) => {
            //判断当前选择的音频输入设备是否为空并且是否有设备
            if (this.state.selectedAudioDevice === "" && data.audioDevices.length > 0) {
                //默认选中第一个设备
                this.state.selectedAudioDevice = data.audioDevices[0].deviceId;
            }
            //判断当前选择的视频输入设备是否为空并且是否有设备
            if (this.state.selectedVideoDevice === "" && data.videoDevices.length > 0) {
                //默认选中第一个设备
                this.state.selectedVideoDevice = data.videoDevices[0].deviceId;
            }
            //设置设备列表状态值
            this.state.videoDevices = data.videoDevices;
            this.state.audioDevices = data.audioDevices;
            this.state.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);
                });
        });
    }

    //音频音量处理
    soundMeterProcess = () => {
        //读取音量值,再乘以一个系数,可以得到音量条的宽度
        var val = (window.soundMeter.instant.toFixed(2) * 348) + 1;
        //设置音量值状态
        this.setState({ audioLevel: val });
        if (this.state.visible) {
            //每隔100毫秒调用一次soundMeterProcess函数,模拟实时检测音频音量
            setTimeout(this.soundMeterProcess, 100);
        }
    }

    //开始预览
    startPreview = () => {
        //判断window对象里是否有stream
        if (window.stream) {
            //关闭音视频流
            this.closeMediaStream(window.stream);
        }
        //SoundMeter声音测量,用于做声音音量测算
        this.soundMeter = window.soundMeter = new SoundMeter(window.audioContext);
        let soundMeterProcess = this.soundMeterProcess;

        //视频预览对象
        let videoElement = this.refs['previewVideo'];
        //音频源
        let audioSource = this.state.selectedAudioDevice;
        //视频源
        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(function (stream) {
                //成功返回音视频流
                window.stream = stream;
                videoElement.srcObject = stream;
                //将声音测量对象与流连接起来
                soundMeter.connectToSource(stream);
                //每隔100毫秒调用一次soundMeterProcess函数,模拟实时检测音频音量
                setTimeout(soundMeterProcess, 100);
                //返回枚举设备
                return navigator.mediaDevices.enumerateDevices();
            })
            .then((devces) => { })
            .catch((erro) => { });

    }

    //停止预览
    stopPreview = () => {
        //关闭音视频流
        if (window.stream) {
            this.closeMediaStream(window.stream);
        }
    }

    //关闭音视频流
    closeMediaStream = (stream) => {
        //判断stream是否为空
        if (!stream) {
            return;
        }
        var tracks, i, len;
        //判断是否有getTracks方法
        if (stream.getTracks) {
            //获取所有Track
            tracks = stream.getTracks();
            //迭代所有Track
            for (i = 0, len = tracks.length; i < len; i += 1) {
                //停止每个Track
                tracks[i].stop();
            }
        } else {
            //获取所有音频Track
            tracks = stream.getAudioTracks();
            //迭代所有音频Track
            for (i = 0, len = tracks.length; i < len; i += 1) {
                //停止每个Track
                tracks[i].stop();
            }
            //获取所有视频Track
            tracks = stream.getVideoTracks();
            //迭代所有视频Track
            for (i = 0, len = tracks.length; i < len; i += 1) {
                //停止每个Track
                tracks[i].stop();
            }
        }
    }

    //弹出对话框
    showModal = () => {
        this.setState({
            visible: true,
        });
        //延迟100毫秒后开始预览
        setTimeout(this.startPreview, 100);
    }

    //点击“确定”按钮进行处理
    handleOk = (e) => {
        //关闭对话框
        this.setState({
            visible: false,
        });
        //判断是否能存储
        if (window.localStorage) {
            //设置信息
            let deviceInfo = {
                //音频设备Id
                audioDevice: this.state.selectedAudioDevice,
                //视频设备Id
                videoDevice: this.state.selectedVideoDevice,
                //分辨率
                resolution: this.state.resolution,
            };
            //使用JSON转成字符串后存储在本地
            localStorage["deviceInfo"] = JSON.stringify(deviceInfo);
        }
        //停止预览
        this.stopPreview();
    }

    //取消设置
    handleCancel = (e) => {
        //关闭对话框
        this.setState({
            visible: false,
        });
        //停止预览
        this.stopPreview();
    }

    //音频输入设备改变
    handleAudioDeviceChange = (e) => {
        console.log('选择的音频输入设备为: ' + JSON.stringify(e));
        this.setState({ selectedAudioDevice: e });
        setTimeout(this.startPreview, 100);
    }
    //视频输入设备改变
    handleVideoDeviceChange = (e) => {
        console.log('选择的视频输入设备为: ' + JSON.stringify(e));
        this.setState({ selectedVideoDevice: e });
        setTimeout(this.startPreview, 100);
    }
    //分辨率选择改变
    handleResolutionChange = (e) => {
        console.log('选择的分辨率为: ' + JSON.stringify(e));
        this.setState({ resolution:e});
    }

    render() {
        return (
            <div className="container">
                <h1>
                    <span>设置综合示例</span>
                </h1>
                <Button onClick={this.showModal}>修改设备</Button>
                <Modal
                    title="修改设备"
                    visible={this.state.visible}
                    onOk={this.handleOk}
                    onCancel={this.handleCancel}
                    okText="确定"
                    cancelText="取消">
                    <div className="item">
                        <span className="item-left">麦克风</span>
                        <div className="item-right">
                            <Select value={this.state.selectedAudioDevice} style= {{ width: 350 }} onChange={this.handleAudioDeviceChange}>
                                {
                                    this.state.audioDevices.map((device, index) => {
                                        return (<Option value={device.deviceId} key={device.deviceId}>{device.label}</Option>);
                                    })
                                }
                            </Select>
                            <div ref="progressbar" style={{
                                width: this.state.audioLevel + 'px',
                                height: '10px',
                                backgroundColor: '#8dc63f',
                                marginTop: '20px',
                            }}>
                            </div>
                        </div>
                    </div>
                    <div className="item">
                        <span className="item-left">摄像头</span>
                        <div className="item-right">
                            <Select value={this.state.selectedVideoDevice} style= {{ width: 350 }} onChange={this.handleVideoDeviceChange}>
                                {
                                    this.state.videoDevices.map((device, index) => {
                                        return (<Option value={device.deviceId} key={device.deviceId}>{device.label}</Option>);
                                    })
                                }
                            </Select>
                            <div className="video-container">
                                <video id='previewVideo' ref='previewVideo' autoPlay playsInline style={{ width: '100%', height: '100%', objectFit: 'contain' }}></video>
                            </div>

                        </div>
                    </div>
                    <div className="item">
                        <span className="item-left">清晰度</span>
                        <div className="item-right">
                            <Select style={{ width: 350 }} value={this.state.resolution} onChange={this.handleResolutionChange}>
                                <Option value="qvga">流畅(320x240)</Option>
                                <Option value="vga">标清(640x360)</Option>
                                <Option value="hd">高清(1280x720)</Option>
                                <Option value="fullhd">超清(1920x1080)</Option>
                            </Select>
                        </div>
                    </div>
                </Modal>
            </div>
        );
    }
}

运行程序后,点击“修改设备”按钮,会打开“修改设备”对话框。运行效果如图5-5所示。

图5-5 设置综合示例效果图

可以选择不同的麦克风测试是否有音量,选择不同的摄像头和清晰度,预览视频。最后点击“确定”按钮,然后,刷新浏览器再次打开对话框,测试是否为之前的设置。另外,当关闭对话框后,查看Chrome浏览器上是否有小红圆圈,以验证设备是否正常关闭。