webRtc生产环境实用方法

最近做了几个项目发现多个项目反反复复会出现几个高频的需求, 都依赖于获取系统采集设备和指定设备id获取媒体流;为了不在反复书写总结2个公用方法;

检索当前系统所有可用设备
typescript 复制代码
/**
   * 检索当前系统所有可用设备
   * @returns {
   *  audioInputOptions: Array [{value: string, label: string}]; 说明: 音频输入设备
   *  videoInputOptions: Array [{value: string, label: string}]; 说明: 视频输入设备
   *  audiooutputOptions: Array [{value: string, label: string}]; 说明: 音频输出设备
   *  isAudioInput: Boolean 是否有音频输入设备;
   *  isVideoInput: Boolean 是否视频输入设备;
   *  isAudioOutput: Boolean 是否有音频输出设备;
   *  audioDeviceIds: [string] // 音频DevIds;
   *  videoDeviceIds: [string] // 视频DevIds;
 * }
*/
export function GetSystemEnumerateDevices() {
    return navigator.mediaDevices.enumerateDevices().then(function(devices) {
        const audioInputOptions = [];
        const videoInputOptions = [];
        const audiooutputOptions = [];
        let isAudioInput = false;
        let isVideoInput = false;
        let isAudioOutput = false;
        const audioDeviceIds = [];
        const videoDeviceIds = [];
        let tmp = devices.filter(function(device) {
            /* 
                *  device.kind: 目前有3个值;
                * 'audioinput': 表示麦克风;
                * 'videoinput': 表示摄像头,没有安装摄像头时部分品牌机({label: 'screen-capture-recorder'},这也就是为啥没摄像头就会传会桌面画面);
                * 'audiooutput': 扬声器/听筒;
            */
           // 判断是否有摄像头设备
            if (device.kind === 'videoinput') {
                if (!isVideoInput) {
                    isVideoInput = true;
                }
                
                // 针对个别电脑优先选择摄像头设备
                if (device.label === 'screen-capture-recorder') {
                    videoDeviceIds.push(device.deviceId);
                    videoInputOptions.push({ value: device.deviceId, label: device.label });
                } else {
                    videoDeviceIds.unshift(device.deviceId);
                    videoInputOptions.unshift({ value: device.deviceId, label: device.label });
                }
            };
            // 判断是否有麦克风设备
            if (device.kind === 'audioinput') {
                if (!isAudioInput) {
                    isAudioInput = true;
                }
                audioInputOptions.push({ value: device.deviceId, label: device.label });
                audioDeviceIds.push(device.deviceId);
            };
            // 音频输出设备
            if (device.kind === 'audiooutput') {
                if (!isAudioOutput) { isAudioOutput = true };
                audiooutputOptions.push({ value: device.deviceId, label: device.label });
            };
            return false;
        });
        tmp = null;
        // console.log('音频输入设备', audioInputOptions);
        // console.log('视频输入设备', videoInputOptions);
        // console.log('音频输出设备', audiooutputOptions);
        // console.log('是否有音频输入设备', isAudioInput);
        // console.log('是否视频输入设备', isVideoInput);
        // console.log('是否有音频输出设备', isAudioOutput);
        // console.log('音频DevIds', audioDeviceIds);
        // console.log('视频DevIds', videoDeviceIds);
        return {audioInputOptions, videoInputOptions, audiooutputOptions, isAudioInput, isVideoInput, isAudioOutput, audioDeviceIds, videoDeviceIds}
    });
}
设备id获取MediaStream
typescript 复制代码
export function DeviceidToMediaStream(devId: string) {
    return new Promise((resolve, reject) => {
        if (typeof devId != 'string') {
            return reject({code: 0, message: '设备id必须是字符串'});
        }
        GetSystemEnumerateDevices().then((devInfo) => {
            const {audioDeviceIds, videoDeviceIds} = devInfo;
            const constraints = {
                audio: false,
                video: false,
            }
            let mediaType = 1;
            if (audioDeviceIds.includes(devId)) {
                constraints.audio = true;
                delete constraints.video;
            }
            if (videoDeviceIds.includes(devInfo)) {
                mediaType = 2;
                constraints.video = true;
                delete constraints.audio;
            }
            let isHas = Object.values(constraints).filter((item) => {
                return item;
            })
            if (isHas.length === 0) {
                return reject({code: 0, message: '没有找到采集设备'});
            }
            navigator.mediaDevices.getUserMedia(constraints).then((locStream) => {
                return resolve(locStream);
            }).catch((error) => {
                const cause = error.message;
                let message = mediaType === 1 ? '麦克风启动失败' : '摄像头启动失败';
                if (cause === 'Permission denied') {
                    message = '媒体采集权限被拒绝';
                } else if (cause === 'Requested device not found') {
                    message = mediaType === 1 ? '未检测到麦克风' : '未检测到采集设备';
                } else if (cause === 'Could not start video source') {
                   // QQ浏览器如果摄像头被其它应用占用会报错: Could not start video source
                    message = '摄像头启动失败,可能被其它应用占用或被拔出';
                } else if (cause === 'Device in use') {
                    // 谷歌浏览器摄像头被其它占用用会报错 'Device in use'
                    message = '设备被另一个应用或进程占用'
                } else if (cause === 'Starting videoinput failed') {
                    // 火狐摄像头被占用会报错 'Starting videoinput failed'
                    message = '设备启动失败,可能被被另一个应用或进程占用'
                };
                return reject({code: 0, message});
            })
        })
    })
}
MediaStream获取视频的宽度和高度
typescript 复制代码
export function GetMediaStreamVideoWidthHeight(streams: MediaStream) {
    return new Promise((resolve, reject) => {
        const videoTrackArr = streams.getVideoTracks();
        if (videoTrackArr.length === 0) {
            reject({code:0, message:'没有视频轨道'});
            return;
        }
        const tmp = document.createElement('video');
        tmp.srcObject = streams;
        tmp.onloadedmetadata = function() {
            resolve({width: tmp.videoWidth, height: tmp.videoHeight});
            tmp.srcObject = null;
            tmp.remove();  
        };
        tmp.onerror = function() {  
            reject({ code: 1, message: '视频加载失败' });  
            tmp.srcObject = null;  
            tmp.remove();  
        };  
    })
}
判断音频录入设备是否存在
typescript 复制代码
export function JudgeAudioInputDevIdIsExist(devId: string) {
    return new Promise((resolve, reject) => {
        if (typeof devId != 'string') {
            return reject({code: 0, message: '设备id必须是字符串'});
        };
        GetSystemEnumerateDevices().then((devInfo) => {
            const {audioDeviceIds} = devInfo;
            if (!audioDeviceIds.includes(devId)) {
                return reject({code: 0, message: '设备id不存在'});
            };
            resolve({code: 1, message: devId});
        });
    })
}
判断视频录入设备是否存在
typescript 复制代码
export function JudgeVideoInputDevIdIsExist(devId: string) {
    return new Promise((resolve, reject) => {
        if (typeof devId != 'string') {
            return reject({code: 0, message: '设备id必须是字符串'});
        };
        GetSystemEnumerateDevices().then((devInfo) => {
            const {videoDeviceIds} = devInfo;
            if (!videoDeviceIds.includes(devId)) {
                return reject({code: 0, message: '设备id不存在'});
            }
            resolve({code: 1, message: devId});
        });
    })
}
拦截音视频权限可能会报错的问题
typescript 复制代码
/**
 * 处理音视频权限错误
 *  mediaType: 1_音频; 2_视频;
*/
export function BackGetUserMediaError(cause: string, mediaType: number) {
    let message = '';
    let mediaName= mediaType === 1 ? '麦克风' : '摄像头'
    if (cause === 'Permission denied') {
        message = `${mediaName}获取权限被拒绝,请检查浏览器是否开启使用权限`;
    } else if (cause === 'Requested device not found') {
        message = `未检测到${mediaName}设备`;
    } else if (cause === 'Could not start video source') {
        /* 
            1.QQ浏览器_设备被占用会走这里; 
            2.QQ浏览器_系统设置关闭摄像头应用使用权限; 
            3.谷歌浏览器_win7或低版本谷歌会存在临时拔掉摄像头但枚举可以拿到设备调用后会报错
        */
        message = '摄像头启动失败,可能被其它应用占用或被拔出或系统关闭了摄像头调用权限';
    } else if (cause === 'Device in use') {
        // 谷歌浏览,假如设备被占用的时候会报错
        message = `${mediaName}设备被另一个应用或进程占用`;
    } else if (cause === 'Starting videoinput failed') {
        // 火狐浏览器, 假如设备被占用的时候会报错
        message = '摄像头启动失败,可能被被另一个应用或进程占用';
    } else if (cause === 'Failed to allocate videosource') {
        // 火狐-win10系统设置关闭摄像头应用使用权限
        message = '摄像头启动失败,可能系统关闭了摄像头使用权限';
    } else if (cause === 'Permission denied by system') {
        /*  存在麦克风摄像头混用报错
            1.谷歌浏览器_win10系统设置关闭摄像头应用使用权限;
            2.谷歌浏览器_win10系统设置关闭麦克风应用使用权限;
            3.QQ浏览器_系统win10系统设置关闭麦克风使用权限
        */
        message = `${mediaName}启动失败,可能系统关闭了设备使用权限`;
    } else {
        message = `${mediaName}启动失败`
    };

    return {code: 0, cause, message};
}
相关推荐
迷雾漫步者1 分钟前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-29 分钟前
验证码机制
前端·后端
燃先生._.1 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖2 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235243 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240253 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar3 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人4 小时前
前端知识补充—CSS
前端·css
GISer_Jing4 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试