WebRTC-设备管理
WebRTC(Web Real-Time Communications)是一项实时通讯技术,它允许网络应用或者站点,在不借助中间媒介的情况下,建立浏览器之间点对点(Peer-to-Peer)的连接,实现视频流和(或)音频流或者其他任意数据的传输。
本文主要介绍关于摄像头、麦克风,扬声器、屏幕共享相关控制和调用。
申请权限与设备开启
// getUserMedia获取设备流的同时,浏览器会发起权限申请。
navigator.mediaDevices.getUserMedia({audio:true,video:true}).then((stream)=>{
// 通过流,获取所有轨道,并终止监听,因为此处仅是为了提前获取权限,所以不需要一开始就监听音视频流
stream.getTracks().forEach((track)=>{
track.stop()
})
}).catch(function (err) {
/* 处理 error */
})
- 火狐浏览器终止流后无法获取设备列表具体信息,如果需要获取设备列表的,
enumerateDevices
获取完后再调用track.stop()
终止监听。 - https站点,或localhost才支持摄像头麦克风的获取,也可以通过配置浏览器设置,跳过校验。
获取设备列表
// 枚举获取所有设备列表,包含扬声器,
navigator.mediaDevices
.enumerateDevices()
.then((devices) => {
devices.forEach((device) => {
// kind 设备类型,"videoinput" 摄像头 "audioinput" 麦克风 "audiooutput" 扬声器
// label 设备名
// deviceId 设备id
console.log(`${device.kind}: ${device.label} id = ${device.deviceId}`);
});
})
.catch((err) => {
console.error(`${err.name}: ${err.message}`);
});
// output:
// videoinput: FaceTime HD Camera (Built-in) id=csO9c0YpAf274OuCPUA53CNE0YHlIr2yXCi+SqfBZZ8=
// audioinput: default (Built-in Microphone) id=RKxXByjnabbADGQNNZqLVLdmXlS0YkETYCIbg+XxnvM=
// audioinput: Built-in Microphone id=r2/xw1xUPIyZunfV1lGrKOma5wTOvCkWfZ368XCndm0=
- 注意,获取设备列表需要先调navigator.mediaDevices.getUserMedia申请权限,否则有可能拿不到设备label名字。
设备开启(监听音视频流)
// getUserMedia获取设备流的同时,浏览器会发起权限申请。
navigator.mediaDevices.getUserMedia({audio:true,video:true}).then((stream)=>{
console.log(stream)
}).catch(function (err) {
/* 处理 error */
})
getUserMedia(constraints:MediaStreamConstraints):Promise<MediaStream>
MediaStreamConstraints
媒体约束对象,指定了请求的媒体类型和相对应的参数,可以调navigator.mediaDevices.getSupportedConstraints()` 查看支持的属性。- 常见用法如下:
// audio 开启麦克风 video 开启摄像头 getUserMedia({audio:true,video:true})
- 也可以传对象,属性为
MediaStreamConstraints
支持的属性
// audio 开启麦克风 video 开启摄像头 getUserMedia({ audio: { // deviceId:'', echoCancellation: true, //音频开启回音消除 // noiseSuppression: true, // 开启降噪 // autoGainControl: true // 开启自动增益功能 }, video: { // deviceId:'' // 如果有多个设备,可以指定开启哪个设备 width: 1280, // 获取的视频流宽度 height: 720, // 获取的视频流高度 // facingMode:'user' // 移动端调用前置摄像头 // facingMode: { exact: "environment" } 移动端调用后置摄像头 } })
设备关闭
// getUserMedia获取设备流的同时,浏览器会发起权限申请。
// navigator.mediaDevices.getUserMedia({audio:true,video:true}).then((stream)=>{
// 获取所有轨道,包含视频轨道和音频轨道
stream.getTracks().forEach((track)=>{
track.stop()
})
// stream.getAudioTracks() 仅获取所有音频轨道
// stream.getVideoTracks() 仅获取所有视频轨道
// stream.addTrack(track) 添加轨道
// stream.removeTrack(track) 移除轨道
// }).catch(function (err) {
/* 处理 error */
// })
stream:MediaStream
媒体流对象,通过getUserMedia
获取,相关api和属性点击查看- 流对象也可以给video标签进行预览
const video = document.querySelector('video')
video.srcObject = stream
- srcObject 通常是MediaStream对象,也可以是MediaSource, Blob 或者 File对象。
扬声器
扬声器的管理比较特殊,它得借助媒体对象(audio,video,AudioContext)才能进行管理,web无法直接操作系统的扬声器,但可以控制媒体对象进行播放,以及扬声器设备的切换。
// audio 音频标签
const audio = document.querySelector('audio')
audio.src = 'https://test.com/xx.mp3'
// 切换扬声器 sinkId 设备id
audio.setSinkId(sinkId)
audio.play()
// video.setSinkId 同理
- HTMLMediaElement的属性和方法,点击查看
- audio和video都是继承的这个对象,所以它有的属性和方法,均可以调用
// AudioContext 音频上下文对象,一般用作音频分析,可以做音频可视化,以及对音频做一些特殊处理
// 创建音频上下文实例
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
// 设置音频来源,可以是 HTMLMediaElement 对象。
const source = audioCtx.createMediaElementSource(document.querySelector('audio'))
// audioCtx.createMediaStreamSource(stream) // 也可以是来自媒体流对象 MediaStream
// 切换扬声器 sinkId 设备id
audioCtx.setSinkId(sinkId)
sinkId
即扬声器的设备id,可以从MediaDevices.enumerateDevices()
获取AudioContext
的属性方法点击查看
屏幕共享
async function startCapture(displayMediaOptions) {
let captureStream = null;
try {
// 开启屏幕捕获,同时会发起权限申请,如果没有权限,返回屏幕捕获的流
captureStream =
await navigator.mediaDevices.getDisplayMedia(displayMediaOptions);
} catch (err) {
console.error("Error: " + err);
}
return captureStream;
}
// 如果指定有audio,浏览器弹窗右下角会有一个是否共享音频的选项
const captureStream = startCapture({video:true,audio:true})
// 拿到捕获流之后,即可通过流取到对应的音视频轨道
let track = captureStream.getVideoTracks()[0]
// captureStream.getAudioTracks 获取所有音频轨道
track.onended = () => {
//监听手动点击“停止分享”
}
// 停止屏幕捕获
track.stop()
getDisplayMedia(constraints:MediaStreamConstraints):Promise<MediaStream>
音频可视化
附上一个封装过的,支持音频文件和麦克风作为来源的可视化vue组件,输出展示为条形
使用
<template>
<!-- 麦克风作为音源 -->
<AudioVisual key="microphone" type="microphone"></AudioVisual>
<!-- 音频文件作为音源 -->
<AudioVisual key="audio" :volume="1" type="audio" audioUrl="/test.mp3"></AudioVisual>
</template>
组件
<!-- audioVisual组件 -->
<template>
<div>
<div class="audio-visual flex flex-cross-center flex-main-between" :style="{width:width+'px',height:height+'px'}">
<div class="audio-bar" :class="i<=maxIndex&&maxIndex!=0?'active':''" v-for="(item,i) in barCount" :key="item">
</div>
</div>
<audio v-if="audioUrl" ref="audio" controls style="display:none"></audio>
</div>
</template>
<script>
// import "" from "@component/"
export default {
name:'audioVisual',
props:{
width:{
type:[String,Number],
default(){
return 2.5
}
},
height:{
type:[String,Number],
default(){
return 0.24
}
},
// microphone 麦克风作为音源,audio 音频文件作为音源
type:{
type:[String],
default(){
return 'microphone'
}
},
volume:{
type:[Number],
default(){
return 1
}
},
audioUrl:{
type:[String],
default(){
return ''
}
},
barCount:{
type:[Number],
default(){
return 13
}
},
microphoneStream:{
type:[MediaStream],
default(){
return null
}
}
},
data(){
return {
maxIndex:0
}
},
watch:{
volume(n,o){
if(n!=o){
if(this.$refs.audio){
this.$refs.audio.volume = this.volume/100
}
}
}
},
created(){
this.$nextTick(()=>{
this.reload()
})
},
mounted(){
},
beforeDestroy(){
this.clear()
if(this.audioCtx && this.audioCtx.state=='running'){
this.$refs.audio && this.$refs.audio.pause()
this.audioCtx.close()
}
},
methods:{
clear(){
this.stopDevice()
clearInterval(this.animateFrame)
},
reload(){
if(this.type =='microphone'){
this.getMicrophone()
}else{
this.initAudio()
}
},
playAudio(){
this.$refs.audio.src= this.audioUrl
this.$refs.audio.currentTime = 0; // 重新播放
this.$refs.audio.volume = this.volume/100
try{
if(this.$refs.audio && this.$refs.audio.paused){
this.$refs.audio.play()
}
}catch(err){
this.$refs.audio.oncanplay=()=>{
if(this.$refs.audio && this.$refs.audio.paused){
this.$refs.audio.play()
}
}
}
},
initAudio(){
this.playAudio()
this.initAudioVirtual(this.$refs.audio)
},
async switchDevice(deviceId){
this.maxIndex = 0
this.stopDevice()
if(this.type=='microphone'){
navigator.mediaDevices
.getUserMedia({ 'audio': {
deviceId:deviceId,
echoCancellation: true, //音频开启回音消除
// noiseSuppression: true, // 开启降噪
// autoGainControl: true // 开启自动增益功能
} })
.then(this.initAudioVirtual)
.catch(e => {
console.error('获取麦克风权限失败', e);
});
}else{
this.$refs.audio.currentTime = 0; // 重新播放
if(deviceId=='default'){
return
}
await this.audioCtx.setSinkId(deviceId)
console.log(this.audioCtx.sinkId)
}
},
getMicrophone(){
this.maxIndex = 0
if(this.microphoneStream){
this.initAudioVirtual(this.microphoneStream)
return
}
navigator.mediaDevices
.getUserMedia({ 'audio': {
echoCancellation: true, //音频开启回音消除
// noiseSuppression: true, // 开启降噪
// autoGainControl: true // 开启自动增益功能
} })
.then(this.initAudioVirtual)
.catch(e => {
console.error('获取麦克风权限失败', e);
});
},
// 关闭设备流监听
stopDevice(){
if(this.stream && this.stream.getAudioTracks && !this.microphoneStream){
this.stream.getAudioTracks().forEach((track)=>{
track.stop()
})
}
if(this.$refs.audio && !this.$refs.audio.paused){
this.$refs.audio.pause()
}
},
initAudioVirtual(audioSource){
// var canvasCtx = document.querySelector('#canvas').getContext('2d');
if(this.audioCtx){
if(this.audioCtx.state=='running'){
this.audioCtx.close()
}
if(this.mediaElementSourceNode){
this.mediaElementSourceNode.disconnect()
}
}
this.audioCtx = new (window.AudioContext || window.webkitAudioContext)();
this.stream = audioSource
// var oscillator = audioCtx.createOscillator();
// oscillator.type = "sine";
// oscillator.frequency.value = 0
let audioCtx = this.audioCtx
var analyser = audioCtx.createAnalyser();
let source = ''
let filter = null
if(this.type=='microphone'){
// 创建滤波器, 过滤回声
filter = audioCtx.createBiquadFilter();
filter.type = 'lowpass'; // 设置为低通滤波器
filter.frequency.value = 1000; // 设置截止频率
source = audioCtx.createMediaStreamSource(audioSource);
source.connect(analyser);
filter.connect(audioCtx.destination)
// analyser.connect(audioCtx.destination);
}else{
source = this.mediaElementSourceNode ? this.mediaElementSourceNode: audioCtx.createMediaElementSource(audioSource);
this.mediaElementSourceNode = source
source.connect(analyser);
analyser.connect(audioCtx.destination);
}
analyser.fftSize = 2048;
// analyser.fftSize analyser.frequencyBinCount
var bufferLength = analyser.frequencyBinCount;
// const WIDTH = this.width, HEIGHT = this.height
// analyser.getByteTimeDomainData(dataArray);
clearInterval(this.animateFrame)
this.animateFrame = setInterval(draw.bind(this), 1000/25);
function draw(){
// 0-255
var dataArray = new Uint8Array(bufferLength);
analyser.getByteFrequencyData(dataArray);
// console.log(Math.max.apply(null,dataArray))
// canvasCtx.clearRect(0, 0, WIDTH, HEIGHT);
// canvasCtx.fillStyle = 'rgb(255, 255, 255)';
// canvasCtx.fillRect(0, 0, WIDTH, HEIGHT);
let BAR_COUNT = this.barCount
// let barWidth = WIDTH * 0.6 /BAR_COUNT;
// let barSpacing = (WIDTH - barWidth * BAR_COUNT) / (BAR_COUNT-1)
// let barHeight = HEIGHT;
// 舍弃了低音的一部分音频数据
let max = Math.max.apply(null,dataArray)
if(max<50){
max = 0
}
if(this.type=='microphone' && this.microphoneStream){
const filterValue = 130
let value = 255-filterValue
max = max - filterValue
if(max<0) max = 0
this.maxIndex = Math.floor(max / (value / BAR_COUNT))
return
}
// console.log(max)
this.maxIndex = Math.floor(max / (255 / BAR_COUNT))
// canvas 画条形图,由于canvas画圆角矩形比较麻烦,转用html的方式
// let x = 0;
// // 画未激活块
// for(let i = 0; i < BAR_COUNT; i++) {
// canvasCtx.fillStyle = this.inactiveColor;
// canvasCtx.fillRect(x,0,barWidth,barHeight);
// x += barSpacing + barWidth;
// }
// x = 0
// // 画激活块
// for(let i = 0; i < BAR_COUNT; i++) {
// // 激活色
// if(i < maxIndex){
// canvasCtx.fillStyle = this.activeColor;
// canvasCtx.fillRect(x,0,barWidth,barHeight);
// }
// x += barSpacing + barWidth;
// }
}
}
}
}
</script>
<style scoped lang="scss">
.audio-bar{
width: 0.08rem;
height: 0.24rem;
background: #E3E5EE;
border-radius: 0.04rem;
&.active{
background: #28D48C;
}
}
</style>